From d5fca1969e096ac7083bb4cbb780d0df81603ead Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 25 Jul 2024 02:59:45 +0200 Subject: [PATCH 001/142] add a script to download prebuilds of monero_c (#1554) --- pubspec_base.yaml | 1 + tool/download_moneroc_prebuilds.dart | 50 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tool/download_moneroc_prebuilds.dart diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 458523fc2..2cfeae716 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -124,6 +124,7 @@ dev_dependencies: git: url: https://github.com/cake-tech/google-translator.git version: 1.0.0 + archive: ^3.6.1 dependency_overrides: bech32: diff --git a/tool/download_moneroc_prebuilds.dart b/tool/download_moneroc_prebuilds.dart new file mode 100644 index 000000000..58e8d4b03 --- /dev/null +++ b/tool/download_moneroc_prebuilds.dart @@ -0,0 +1,50 @@ +import 'package:dio/dio.dart'; +import 'package:archive/archive_io.dart'; + +final _dio = Dio(); + +final List triplets = [ + "x86_64-linux-gnu", // linux desktop - majority of users onlinux + // "i686-linux-gnu", // not supported by cake + // "i686-meego-linux-gnu", // sailfishos (emulator)- not supported by cake + // "aarch64-linux-gnu", // not (yet) supported by cake - (mostly) mobile linux + // "aarch64-meego-linux-gnu", // sailfishos - not supported by cake + "x86_64-linux-android", + // "i686-linux-android", // not supported by monero_c - mostly old android emulators + "aarch64-linux-android", + "armv7a-linux-androideabi", + // "i686-w64-mingw32", // 32bit windows - not supported by monero_c + "x86_64-w64-mingw32", + // "x86_64-apple-darwin11", // Intel macbooks (contrib) - not used by cake + // "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-host-apple-darwin", // apple silicon macbooks (local builds) + "host-apple-ios", +]; + +Future main() async { + final resp = await _dio.get("https://api.github.com/repos/mrcyjanek/monero_c/releases"); + final data = resp.data[0]; + final tagName = data['tag_name']; + print("Downloading artifacts for: ${tagName}"); + final assets = data['assets'] as List; + for (var i = 0; i < assets.length; i++) { + for (var triplet in triplets) { + final asset = assets[i]; + final filename = asset["name"] as String; + if (!filename.contains(triplet)) continue; + final coin = filename.split("_")[0]; + String localFilename = filename.replaceAll("${coin}_${triplet}_", ""); + localFilename = "scripts/monero_c/release/${coin}/${triplet}_${localFilename}"; + final url = asset["browser_download_url"] as String; + print("- downloading $localFilename"); + await _dio.download(url, localFilename); + print(" extracting $localFilename"); + final inputStream = InputFileStream(localFilename); + final archive = XZDecoder().decodeBuffer(inputStream); + final outputStream = OutputFileStream(localFilename.replaceAll(".xz", "")); + outputStream.writeBytes(archive); + } + } +} \ No newline at end of file From eb8158e9963b6dc518fbb24ba5bd36251e5c47f7 Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 26 Jul 2024 17:36:18 +0200 Subject: [PATCH 002/142] fix invalid wallet pointer getting set even when we throw (#1556) * fix invalid wallet pointer getting set even when we throw * Hide "wallet seeds" field if there is no seeds (cases of restore from keys) --------- Co-authored-by: OmarHatem --- cw_monero/lib/api/wallet_manager.dart | 37 +++++++++++-------- cw_wownero/lib/api/wallet_manager.dart | 41 +++++++++++++--------- lib/view_model/wallet_keys_view_model.dart | 12 +++---- 3 files changed, 53 insertions(+), 37 deletions(-) diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 02ce2b7d6..50ab41e04 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -32,13 +32,14 @@ void createWalletSync( required String language, int nettype = 0}) { txhistory = null; - wptr = monero.WalletManager_createWallet(wmPtr, + final newWptr = monero.WalletManager_createWallet(wmPtr, path: path, password: password, language: language, networkType: 0); - final status = monero.Wallet_status(wptr!); + final status = monero.Wallet_status(newWptr); if (status != 0) { - throw WalletCreationException(message: monero.Wallet_errorString(wptr!)); + throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); } + wptr = newWptr; monero.Wallet_store(wptr!, path: path); openedWalletsByPath[path] = wptr!; @@ -57,7 +58,7 @@ void restoreWalletFromSeedSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - wptr = monero.WalletManager_recoveryWallet( + final newWptr = monero.WalletManager_recoveryWallet( wmPtr, path: path, password: password, @@ -67,12 +68,13 @@ void restoreWalletFromSeedSync( networkType: 0, ); - final status = monero.Wallet_status(wptr!); + final status = monero.Wallet_status(newWptr); if (status != 0) { - final error = monero.Wallet_errorString(wptr!); + final error = monero.Wallet_errorString(newWptr); throw WalletRestoreFromSeedException(message: error); } + wptr = newWptr; openedWalletsByPath[path] = wptr!; } @@ -87,7 +89,7 @@ void restoreWalletFromKeysSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - wptr = monero.WalletManager_createWalletFromKeys( + final newWptr = monero.WalletManager_createWalletFromKeys( wmPtr, path: path, password: password, @@ -98,12 +100,14 @@ void restoreWalletFromKeysSync( nettype: 0, ); - final status = monero.Wallet_status(wptr!); + final status = monero.Wallet_status(newWptr); if (status != 0) { throw WalletRestoreFromKeysException( - message: monero.Wallet_errorString(wptr!)); + message: monero.Wallet_errorString(newWptr)); } + wptr = newWptr; + openedWalletsByPath[path] = wptr!; } @@ -128,7 +132,7 @@ void restoreWalletFromSpendKeySync( // ); txhistory = null; - wptr = monero.WalletManager_createDeterministicWalletFromSpendKey( + final newWptr = monero.WalletManager_createDeterministicWalletFromSpendKey( wmPtr, path: path, password: password, @@ -138,14 +142,16 @@ void restoreWalletFromSpendKeySync( restoreHeight: restoreHeight, ); - final status = monero.Wallet_status(wptr!); + final status = monero.Wallet_status(newWptr); if (status != 0) { - final err = monero.Wallet_errorString(wptr!); + final err = monero.Wallet_errorString(newWptr); print("err: $err"); throw WalletRestoreFromKeysException(message: err); } + wptr = newWptr; + monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); storeSync(); @@ -203,15 +209,16 @@ void loadWallet( }); } txhistory = null; - wptr = monero.WalletManager_openWallet(wmPtr, + final newWptr = monero.WalletManager_openWallet(wmPtr, path: path, password: password); _lastOpenedWallet = path; - final status = monero.Wallet_status(wptr!); + final status = monero.Wallet_status(newWptr); if (status != 0) { - final err = monero.Wallet_errorString(wptr!); + final err = monero.Wallet_errorString(newWptr); print(err); throw WalletOpeningException(message: err); } + wptr = newWptr; openedWalletsByPath[path] = wptr!; } } diff --git a/cw_wownero/lib/api/wallet_manager.dart b/cw_wownero/lib/api/wallet_manager.dart index 68d0796f9..afcc536e7 100644 --- a/cw_wownero/lib/api/wallet_manager.dart +++ b/cw_wownero/lib/api/wallet_manager.dart @@ -32,13 +32,14 @@ void createWalletSync( required String language, int nettype = 0}) { txhistory = null; - wptr = wownero.WalletManager_createWallet(wmPtr, + final newWptr = wownero.WalletManager_createWallet(wmPtr, path: path, password: password, language: language, networkType: 0); - final status = wownero.Wallet_status(wptr!); + final status = wownero.Wallet_status(newWptr); if (status != 0) { - throw WalletCreationException(message: wownero.Wallet_errorString(wptr!)); + throw WalletCreationException(message: wownero.Wallet_errorString(newWptr)); } + wptr = newWptr; wownero.Wallet_store(wptr!, path: path); openedWalletsByPath[path] = wptr!; @@ -56,9 +57,10 @@ void restoreWalletFromSeedSync( required String seed, int nettype = 0, int restoreHeight = 0}) { + var newWptr; if (seed.split(" ").length == 14) { txhistory = null; - wptr = wownero.WOWNERO_deprecated_restore14WordSeed( + newWptr = wownero.WOWNERO_deprecated_restore14WordSeed( path: path, password: password, language: seed, // I KNOW - this is supposed to be called seed @@ -70,7 +72,7 @@ void restoreWalletFromSeedSync( ); } else { txhistory = null; - wptr = wownero.WalletManager_recoveryWallet( + newWptr = wownero.WalletManager_recoveryWallet( wmPtr, path: path, password: password, @@ -81,13 +83,15 @@ void restoreWalletFromSeedSync( ); } - final status = wownero.Wallet_status(wptr!); + final status = wownero.Wallet_status(newWptr); if (status != 0) { - final error = wownero.Wallet_errorString(wptr!); + final error = wownero.Wallet_errorString(newWptr); throw WalletRestoreFromSeedException(message: error); } + wptr = newWptr; + openedWalletsByPath[path] = wptr!; } @@ -101,7 +105,7 @@ void restoreWalletFromKeysSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - wptr = wownero.WalletManager_createWalletFromKeys( + final newWptr = wownero.WalletManager_createWalletFromKeys( wmPtr, path: path, password: password, @@ -112,12 +116,14 @@ void restoreWalletFromKeysSync( nettype: 0, ); - final status = wownero.Wallet_status(wptr!); + final status = wownero.Wallet_status(newWptr); if (status != 0) { throw WalletRestoreFromKeysException( - message: wownero.Wallet_errorString(wptr!)); + message: wownero.Wallet_errorString(newWptr)); } + wptr = newWptr; + openedWalletsByPath[path] = wptr!; } @@ -142,7 +148,7 @@ void restoreWalletFromSpendKeySync( // ); txhistory = null; - wptr = wownero.WalletManager_createDeterministicWalletFromSpendKey( + final newWptr = wownero.WalletManager_createDeterministicWalletFromSpendKey( wmPtr, path: path, password: password, @@ -152,14 +158,16 @@ void restoreWalletFromSpendKeySync( restoreHeight: restoreHeight, ); - final status = wownero.Wallet_status(wptr!); + final status = wownero.Wallet_status(newWptr); if (status != 0) { - final err = wownero.Wallet_errorString(wptr!); + final err = wownero.Wallet_errorString(newWptr); print("err: $err"); throw WalletRestoreFromKeysException(message: err); } + wptr = newWptr; + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); storeSync(); @@ -217,15 +225,16 @@ void loadWallet( }); } txhistory = null; - wptr = wownero.WalletManager_openWallet(wmPtr, + final newWptr = wownero.WalletManager_openWallet(wmPtr, path: path, password: password); _lastOpenedWallet = path; - final status = wownero.Wallet_status(wptr!); + final status = wownero.Wallet_status(newWptr); if (status != 0) { - final err = wownero.Wallet_errorString(wptr!); + final err = wownero.Wallet_errorString(newWptr); print(err); throw WalletOpeningException(message: err); } + wptr = newWptr; openedWalletsByPath[path] = wptr!; } } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 511822601..1d5c27fed 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -83,8 +83,8 @@ abstract class WalletKeysViewModelBase with Store { StandartListItem( title: S.current.view_key_private, value: keys['privateViewKey']!), - StandartListItem( - title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + if (_appStore.wallet!.seed!.isNotEmpty) + StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), ]); if (_appStore.wallet?.seed != null && @@ -123,8 +123,8 @@ abstract class WalletKeysViewModelBase with Store { StandartListItem( title: S.current.view_key_private, value: keys['privateViewKey']!), - StandartListItem( - title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + if (_appStore.wallet!.seed!.isNotEmpty) + StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), ]); } @@ -147,8 +147,8 @@ abstract class WalletKeysViewModelBase with Store { StandartListItem( title: S.current.view_key_private, value: keys['privateViewKey']!), - StandartListItem( - title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + if (_appStore.wallet!.seed!.isNotEmpty) + StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), ]); if (_appStore.wallet?.seed != null && From a2549b42b04fbce33b77dbf3fcdd4d6e711ac192 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Wed, 31 Jul 2024 02:26:56 +0100 Subject: [PATCH 003/142] CW-680: Fix Wakelock Issue (#1557) * chore: Bump up wakelock_plus dependency version * Fix: try fixing ci failure by bumping jdk version --- .github/workflows/pr_test_build.yml | 2 +- pubspec_base.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index f37919e9d..4c46137ac 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -41,7 +41,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: "11.x" + java-version: "17.x" - name: Configure placeholder git details run: | git config --global user.email "CI@cakewallet.com" diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 2cfeae716..67a162674 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -66,7 +66,7 @@ dependencies: url: https://github.com/cake-tech/device_display_brightness.git ref: master workmanager: ^0.5.1 - wakelock_plus: ^1.1.3 + wakelock_plus: ^1.2.5 flutter_mailer: ^2.0.2 device_info_plus: ^9.1.0 base32: 2.1.3 From 9da9bee384588675dda11cf28b58e74d0ac7a030 Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 6 Aug 2024 13:01:38 +0200 Subject: [PATCH 004/142] make the error more readable when node fails to respond (#1570) --- cw_monero/lib/api/transaction_history.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 5e33c6c56..c28f162be 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -110,7 +110,10 @@ Future createTransactionSync( })(); if (error != null) { - final message = error; + String message = error; + if (message.contains("RPC error")) { + message = "Invalid node response, please try again or switch node\n\ntrace: $message"; + } throw CreationTransactionException(message: message); } From 5e944a8bf7069f0b083f177c44d1b4e5eb1f265e Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 6 Aug 2024 17:59:44 +0300 Subject: [PATCH 005/142] Try to show seeds if wallet files gets corrupted (#1567) * add litecoin nodes minor ui fix * Try to open the wallet or fetch the seeds and show them to the user * make sure the seeds are only displayed after authentication --- assets/litecoin_electrum_server_list.yml | 17 ++++++- cw_core/lib/wallet_service.dart | 19 +++++++ cw_monero/lib/monero_wallet.dart | 1 - cw_monero/lib/monero_wallet_service.dart | 37 +++++++++++--- lib/core/wallet_loading_service.dart | 31 ++++++++++- lib/di.dart | 14 +++++ .../on_authentication_state_change.dart | 14 +++++ .../monero_account_edit_or_create_page.dart | 4 +- lib/utils/exception_handler.dart | 51 +++++++++++++++++++ 9 files changed, 175 insertions(+), 13 deletions(-) diff --git a/assets/litecoin_electrum_server_list.yml b/assets/litecoin_electrum_server_list.yml index 991762885..550b900e1 100644 --- a/assets/litecoin_electrum_server_list.yml +++ b/assets/litecoin_electrum_server_list.yml @@ -1,4 +1,19 @@ - uri: ltc-electrum.cakewallet.com:50002 useSSL: true - isDefault: true \ No newline at end of file + isDefault: true +- + uri: litecoin.stackwallet.com:20063 + useSSL: true +- + uri: electrum-ltc.bysh.me:50002 + useSSL: true +- + uri: lightweight.fiatfaucet.com:50002 + useSSL: true +- + uri: electrum.ltc.xurious.com:50002 + useSSL: true +- + uri: backup.electrum-ltc.org:443 + useSSL: true diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index fcbd59ff3..d90ae30bc 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'dart:io'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_type.dart'; @@ -42,4 +44,21 @@ abstract class WalletService getSeeds(String name, String password, WalletType type) async { + try { + final path = await pathForWallet(name: name, type: type); + final jsonSource = await read(path: path, password: password); + try { + final data = json.decode(jsonSource) as Map; + return data['mnemonic'] as String? ?? ''; + } catch (_) { + // if not a valid json + return jsonSource.substring(0, 200); + } + } catch (_) { + // if the file couldn't be opened or read + return ''; + } + } } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 4b596648e..b8e3c2765 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -19,7 +19,6 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index ea2f3b766..3588ebb78 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -57,8 +57,11 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { final String spendKey; } -class MoneroWalletService extends WalletService { +class MoneroWalletService extends WalletService< + MoneroNewWalletCredentials, + MoneroRestoreWalletFromSeedCredentials, + MoneroRestoreWalletFromKeysCredentials, + MoneroNewWalletCredentials> { MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -183,11 +186,8 @@ class MoneroWalletService extends WalletService restoreFromHardwareWallet(MoneroNewWalletCredentials credentials) { - throw UnimplementedError("Restoring a Monero wallet from a hardware wallet is not yet supported!"); + throw UnimplementedError( + "Restoring a Monero wallet from a hardware wallet is not yet supported!"); } @override @@ -350,4 +351,24 @@ class MoneroWalletService extends WalletService getSeeds(String name, String password, WalletType type) async { + try { + final path = await pathForWallet(name: name, type: getType()); + + if (walletFilesExist(path)) { + await repairOldAndroidWallet(name); + } + + await monero_wallet_manager.openWalletAsync({'path': path, 'password': password}); + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + return wallet.seed; + } catch (_) { + // if the file couldn't be opened or read + return ''; + } + } } diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 1f17a7a1c..ca29576e4 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -1,6 +1,9 @@ +import 'dart:async'; + import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/wallet_base.dart'; @@ -52,6 +55,12 @@ class WalletLoadingService { } catch (error, stack) { ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack)); + // try fetching the seeds of the corrupted wallet to show it to the user + String corruptedWalletsSeeds = "Corrupted wallets seeds (if retrievable, empty otherwise):"; + try { + corruptedWalletsSeeds += await _getCorruptedWalletSeeds(name, type); + } catch (_) {} + // try opening another wallet that is not corrupted to give user access to the app final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); @@ -69,12 +78,23 @@ class WalletLoadingService { await sharedPreferences.setInt( PreferencesKey.currentWalletType, serializeToInt(wallet.type)); + // if found a wallet that is not corrupted, then still display the seeds of the corrupted ones + authenticatedErrorStreamController.add(corruptedWalletsSeeds); + return wallet; - } catch (_) {} + } catch (_) { + // save seeds and show corrupted wallets' seeds to the user + try { + final seeds = await _getCorruptedWalletSeeds(walletInfo.name, walletInfo.type); + if (!corruptedWalletsSeeds.contains(seeds)) { + corruptedWalletsSeeds += seeds; + } + } catch (_) {} + } } // if all user's wallets are corrupted throw exception - throw error; + throw error.toString() + "\n\n" + corruptedWalletsSeeds; } } @@ -96,4 +116,11 @@ class WalletLoadingService { isPasswordUpdated = true; await sharedPreferences.setBool(key, isPasswordUpdated); } + + Future _getCorruptedWalletSeeds(String name, WalletType type) async { + final walletService = walletServiceFactory.call(type); + final password = await keyService.getWalletPassword(walletName: name); + + return "\n\n$type ($name): ${await walletService.getSeeds(name, password, type)}"; + } } diff --git a/lib/di.dart b/lib/di.dart index 1462370fc..a37574f21 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,3 +1,5 @@ +import 'dart:async' show Timer; + import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/anonpay/anonpay_api.dart'; import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; @@ -487,6 +489,7 @@ Future setup({ if (loginError != null) { authPageState.changeProcessText('ERROR: ${loginError.toString()}'); + loginError = null; } ReactionDisposer? _reaction; @@ -498,6 +501,17 @@ Future setup({ linkViewModel.handleLink(); } }); + + Timer.periodic(Duration(seconds: 1), (timer) { + if (timer.tick > 30) { + timer.cancel(); + } + + if (loginError != null) { + authPageState.changeProcessText('ERROR: ${loginError.toString()}'); + timer.cancel(); + } + }); } }); }); diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 5f1214b76..e4fd9b32f 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:flutter/widgets.dart'; @@ -8,9 +10,16 @@ import 'package:cake_wallet/store/authentication_store.dart'; ReactionDisposer? _onAuthenticationStateChange; dynamic loginError; +StreamController authenticatedErrorStreamController = StreamController(); void startAuthenticationStateChange( AuthenticationStore authenticationStore, GlobalKey navigatorKey) { + authenticatedErrorStreamController.stream.listen((event) { + if (authenticationStore.state == AuthenticationState.allowed) { + ExceptionHandler.showError(event.toString(), delayInSeconds: 3); + } + }); + _onAuthenticationStateChange ??= autorun((_) async { final state = authenticationStore.state; @@ -26,6 +35,11 @@ void startAuthenticationStateChange( if (state == AuthenticationState.allowed) { await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + if (!(await authenticatedErrorStreamController.stream.isEmpty)) { + ExceptionHandler.showError( + (await authenticatedErrorStreamController.stream.first).toString()); + authenticatedErrorStreamController.stream.drain(); + } return; } }); diff --git a/lib/src/screens/monero_accounts/monero_account_edit_or_create_page.dart b/lib/src/screens/monero_accounts/monero_account_edit_or_create_page.dart index 779628be8..2c9918d74 100644 --- a/lib/src/screens/monero_accounts/monero_account_edit_or_create_page.dart +++ b/lib/src/screens/monero_accounts/monero_account_edit_or_create_page.dart @@ -51,7 +51,9 @@ class MoneroAccountEditOrCreatePage extends BasePage { await moneroAccountCreationViewModel.save(); - Navigator.of(context).pop(_textController.text); + if (context.mounted) { + Navigator.of(context).pop(_textController.text); + } }, text: moneroAccountCreationViewModel.isEdit ? S.of(context).rename diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index b19b1bb7e..6045c0004 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -4,11 +4,13 @@ import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/root_dir.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_mailer/flutter_mailer.dart'; import 'package:cake_wallet/utils/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -254,4 +256,53 @@ class ExceptionHandler { 'productName': data.productName, }; } + + static void showError(String error, {int? delayInSeconds}) async { + if (_hasError) { + return; + } + _hasError = true; + + if (delayInSeconds != null) { + Future.delayed(Duration(seconds: delayInSeconds), () => _showCopyPopup(error)); + return; + } + + WidgetsBinding.instance.addPostFrameCallback( + (_) async => _showCopyPopup(error), + ); + } + + static Future _showCopyPopup(String content) async { + if (navigatorKey.currentContext != null) { + final shouldCopy = await showPopUp( + context: navigatorKey.currentContext!, + builder: (context) { + return AlertWithTwoActions( + isDividerExist: true, + alertTitle: S.of(context).error, + alertContent: content, + rightButtonText: S.of(context).copy, + leftButtonText: S.of(context).close, + actionRightButton: () { + Navigator.of(context).pop(true); + }, + actionLeftButton: () { + Navigator.of(context).pop(); + }, + ); + }, + ); + + if (shouldCopy == true) { + await Clipboard.setData(ClipboardData(text: content)); + await showBar( + navigatorKey.currentContext!, + S.of(navigatorKey.currentContext!).copied_to_clipboard, + ); + } + } + + _hasError = false; + } } From e58d87e94cc82e268fc5e5901e4bea47c7c4ce50 Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 7 Aug 2024 13:40:31 +0200 Subject: [PATCH 006/142] add card for when monero wallet is in broken state (#1578) --- .../screens/dashboard/pages/balance_page.dart | 14 ++++++++++++++ .../dashboard/dashboard_view_model.dart | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index d95c19dad..11abdeb58 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -250,6 +250,20 @@ class CryptoBalanceWidget extends StatelessWidget { Observer(builder: (context) { return Column( children: [ + if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: "Monero wallet is broken", + subTitle: "Here are the things that are broken:\n - " + +dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") + +"\n\nPlease restart your wallet and if it doesn't help contact our support.", + onTap: () {}, + ) + ) + ], if (dashboardViewModel.showSilentPaymentsCard) ...[ SizedBox(height: 10), Padding( diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 5b5353e06..06c565035 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -335,6 +335,23 @@ abstract class DashboardViewModelBase with Store { wallet.type == WalletType.wownero || wallet.type == WalletType.haven; + @computed + List get isMoneroWalletBrokenReasons { + if (wallet.type != WalletType.monero) return []; + final keys = monero!.getKeys(wallet); + List errors = [ + if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0", + if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("")) "private view key is 0", + if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0", + if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) "private view key is 0", + if (wallet.seed == null) "wallet seed is null", + if (wallet.seed == "") "wallet seed is empty", + if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address == "41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4") + "primary address is invalid, you won't be able to receive / spend funds", + ]; + return errors; + } + @computed bool get hasSilentPayments => wallet.type == WalletType.bitcoin && !wallet.isHardwareWallet; From 96e4a4eb6c41a0a3c541a617073096ef96c1492f Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 7 Aug 2024 18:12:49 +0200 Subject: [PATCH 007/142] monero fixes (#1581) * correct comparision while syncing * fix issue from report 25916.txt * return proper address even if numSubaddresses returned 0 --- cw_monero/lib/api/subaddress_list.dart | 6 +++++- cw_monero/lib/api/wallet.dart | 2 +- cw_monero/lib/monero_wallet_addresses.dart | 2 +- cw_wownero/lib/api/subaddress_list.dart | 6 +++++- cw_wownero/lib/wownero_wallet_addresses.dart | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cw_monero/lib/api/subaddress_list.dart b/cw_monero/lib/api/subaddress_list.dart index 57edea76e..e5145692d 100644 --- a/cw_monero/lib/api/subaddress_list.dart +++ b/cw_monero/lib/api/subaddress_list.dart @@ -42,12 +42,16 @@ class Subaddress { List getAllSubaddresses() { final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); - return List.generate(size, (index) { + final list = List.generate(size, (index) { return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, ); }).reversed.toList(); + if (list.length == 0) { + list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0)); + } + return list; } void addSubaddressSync({required int accountIndex, required String label}) { diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 6ca9cd1bb..0f6e59c4e 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -131,7 +131,7 @@ void storeSync() async { return monero.Wallet_synchronized(Pointer.fromAddress(addr)); }); if (lastStorePointer == wptr!.address && - lastStoreHeight + 5000 < monero.Wallet_blockChainHeight(wptr!) && + lastStoreHeight + 5000 > monero.Wallet_blockChainHeight(wptr!) && !synchronized) { return; } diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index f74e7dd5b..d4f22e46f 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -109,7 +109,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; address = subaddress!.address; } diff --git a/cw_wownero/lib/api/subaddress_list.dart b/cw_wownero/lib/api/subaddress_list.dart index cec7d94cb..d8c91a584 100644 --- a/cw_wownero/lib/api/subaddress_list.dart +++ b/cw_wownero/lib/api/subaddress_list.dart @@ -41,12 +41,16 @@ class Subaddress { List getAllSubaddresses() { final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); - return List.generate(size, (index) { + final list = List.generate(size, (index) { return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, ); }).reversed.toList(); + if (list.isEmpty) { + list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex)); + } + return list; } void addSubaddressSync({required int accountIndex, required String label}) { diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index dc4b42840..9eeb182eb 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -109,7 +109,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; address = subaddress!.address; } From 15d88e0f8dc8b9b3b055d913bb64e42ed5e322e0 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 8 Aug 2024 12:17:17 +0200 Subject: [PATCH 008/142] Add Ledger Flex Support (#1576) --- cw_bitcoin/pubspec.yaml | 4 ++++ cw_evm/pubspec.yaml | 2 +- pubspec_base.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 66c5729e8..69ff3d29b 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -56,6 +56,10 @@ dev_dependencies: hive_generator: ^1.1.3 dependency_overrides: + ledger_flutter: + git: + url: https://github.com/cake-tech/ledger-flutter.git + ref: cake-v3 watcher: ^1.1.0 # For information on the generic Dart part of this file, see the diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index e4b29b676..c3f4347c2 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -35,7 +35,7 @@ dependency_overrides: ledger_flutter: git: url: https://github.com/cake-tech/ledger-flutter.git - ref: cake + ref: cake-v3 watcher: ^1.1.0 dev_dependencies: diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 67a162674..84b4631fc 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -133,7 +133,7 @@ dependency_overrides: ledger_flutter: git: url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-stax + ref: cake-v3 web3dart: git: url: https://github.com/cake-tech/web3dart.git From ba433ef6f30442e2ac69724fdcb4415699f3db8d Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Thu, 8 Aug 2024 03:27:04 -0700 Subject: [PATCH 009/142] Request timeout fix (#1584) * always handle RequestFailedTimeoutException * undo change that was for testing --- cw_bitcoin/lib/electrum.dart | 69 +++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index b52015794..e3925ca74 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -236,25 +236,37 @@ class ElectrumClient { return []; }); - Future> getTransactionRaw({required String hash}) async => - callWithTimeout(method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000) - .then((dynamic result) { - if (result is Map) { - return result; - } + Future> getTransactionRaw({required String hash}) async { + try { + final result = await callWithTimeout( + method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000); + if (result is Map) { + return result; + } + } on RequestFailedTimeoutException catch (_) { + return {}; + } catch (e) { + print("getTransactionRaw: ${e.toString()}"); + return {}; + } + return {}; + } - return {}; - }); - - Future getTransactionHex({required String hash}) async => - callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000) - .then((dynamic result) { - if (result is String) { - return result; - } - - return ''; - }); + Future getTransactionHex({required String hash}) async { + try { + final result = await callWithTimeout( + method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000); + if (result is String) { + return result; + } + } on RequestFailedTimeoutException catch (_) { + return ''; + } catch (e) { + print("getTransactionHex: ${e.toString()}"); + return ''; + } + return ''; + } Future broadcastTransaction( {required String transactionRaw, @@ -353,14 +365,21 @@ class ElectrumClient { // "height": 520481, // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" // } - Future getCurrentBlockChainTip() => - callWithTimeout(method: 'blockchain.headers.subscribe').then((result) { - if (result is Map) { - return result["height"] as int; - } - return null; - }); + Future getCurrentBlockChainTip() async { + try { + final result = await callWithTimeout(method: 'blockchain.headers.subscribe'); + if (result is Map) { + return result["height"] as int; + } + return null; + } on RequestFailedTimeoutException catch (_) { + return null; + } catch (e) { + print("getCurrentBlockChainTip: ${e.toString()}"); + return null; + } + } BehaviorSubject? chainTipSubscribe() { _id += 1; From 8e7233b5c39faf19775989437649125b7563450d Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Fri, 9 Aug 2024 21:15:54 +0200 Subject: [PATCH 010/142] Monero stability and cleanup (#1572) * migrate monero.dart from it's own repository to monero_c * show errors when invalid monero_c library is being used * Delete unused code * Delete unused code * Fix potential bug causing missing Polyseeds and tx-keys; Add Waring * Remove unused wownero-code * bump monero_c commit --------- Co-authored-by: Czarek Nakamoto Co-authored-by: Omar Hatem --- cw_core/lib/monero_wallet_utils.dart | 5 +- .../connection_to_node_exception.dart | 5 - cw_monero/lib/api/structs/account_row.dart | 12 - cw_monero/lib/api/structs/coins_info_row.dart | 73 - cw_monero/lib/api/structs/subaddress_row.dart | 15 - .../lib/api/structs/transaction_info_row.dart | 41 - cw_monero/lib/api/structs/ut8_box.dart | 8 - cw_monero/lib/api/transaction_history.dart | 5 +- cw_monero/lib/api/wallet_manager.dart | 35 + cw_monero/lib/cw_monero.dart | 8 - cw_monero/lib/cw_monero_method_channel.dart | 17 - .../lib/cw_monero_platform_interface.dart | 29 - cw_monero/lib/monero_transaction_info.dart | 23 - cw_monero/lib/monero_unspent.dart | 9 - cw_monero/lib/monero_wallet.dart | 24 +- cw_monero/lib/mymonero.dart | 1689 ----------------- cw_monero/pubspec.lock | 200 +- cw_monero/pubspec.yaml | 5 +- .../connection_to_node_exception.dart | 5 - cw_wownero/lib/api/structs/account_row.dart | 12 - .../lib/api/structs/coins_info_row.dart | 73 - .../lib/api/structs/subaddress_row.dart | 15 - .../lib/api/structs/transaction_info_row.dart | 41 - cw_wownero/lib/api/structs/ut8_box.dart | 8 - cw_wownero/lib/api/transaction_history.dart | 4 +- cw_wownero/lib/api/wallet_manager.dart | 35 + cw_wownero/lib/cw_wownero.dart | 8 - cw_wownero/lib/cw_wownero_method_channel.dart | 17 - .../lib/cw_wownero_platform_interface.dart | 29 - cw_wownero/lib/mywownero.dart | 1689 ----------------- cw_wownero/lib/wownero_transaction_info.dart | 21 - cw_wownero/lib/wownero_unspent.dart | 9 - cw_wownero/lib/wownero_wallet.dart | 22 +- cw_wownero/pubspec.lock | 8 +- cw_wownero/pubspec.yaml | 5 +- lib/monero/cw_monero.dart | 5 + .../screens/dashboard/pages/balance_page.dart | 30 + .../dashboard/dashboard_view_model.dart | 22 + lib/wownero/cw_wownero.dart | 5 + scripts/prepare_moneroc.sh | 2 +- tool/configure.dart | 4 + 41 files changed, 279 insertions(+), 3993 deletions(-) delete mode 100644 cw_monero/lib/api/exceptions/connection_to_node_exception.dart delete mode 100644 cw_monero/lib/api/structs/account_row.dart delete mode 100644 cw_monero/lib/api/structs/coins_info_row.dart delete mode 100644 cw_monero/lib/api/structs/subaddress_row.dart delete mode 100644 cw_monero/lib/api/structs/transaction_info_row.dart delete mode 100644 cw_monero/lib/api/structs/ut8_box.dart delete mode 100644 cw_monero/lib/cw_monero.dart delete mode 100644 cw_monero/lib/cw_monero_method_channel.dart delete mode 100644 cw_monero/lib/cw_monero_platform_interface.dart delete mode 100644 cw_monero/lib/mymonero.dart delete mode 100644 cw_wownero/lib/api/exceptions/connection_to_node_exception.dart delete mode 100644 cw_wownero/lib/api/structs/account_row.dart delete mode 100644 cw_wownero/lib/api/structs/coins_info_row.dart delete mode 100644 cw_wownero/lib/api/structs/subaddress_row.dart delete mode 100644 cw_wownero/lib/api/structs/transaction_info_row.dart delete mode 100644 cw_wownero/lib/api/structs/ut8_box.dart delete mode 100644 cw_wownero/lib/cw_wownero.dart delete mode 100644 cw_wownero/lib/cw_wownero_method_channel.dart delete mode 100644 cw_wownero/lib/cw_wownero_platform_interface.dart delete mode 100644 cw_wownero/lib/mywownero.dart diff --git a/cw_core/lib/monero_wallet_utils.dart b/cw_core/lib/monero_wallet_utils.dart index 1b1988eb6..8a4990f78 100644 --- a/cw_core/lib/monero_wallet_utils.dart +++ b/cw_core/lib/monero_wallet_utils.dart @@ -79,6 +79,7 @@ Future backupWalletFilesExists(String name) async { backupAddressListFile.existsSync(); } +// WARNING: Transaction keys and your Polyseed CANNOT be recovered if this file is deleted Future removeCache(String name) async { final path = await pathForWallet(name: name, type: WalletType.monero); final cacheFile = File(path); @@ -92,8 +93,8 @@ Future restoreOrResetWalletFiles(String name) async { final backupsExists = await backupWalletFilesExists(name); if (backupsExists) { + await removeCache(name); + await restoreWalletFiles(name); } - - removeCache(name); } diff --git a/cw_monero/lib/api/exceptions/connection_to_node_exception.dart b/cw_monero/lib/api/exceptions/connection_to_node_exception.dart deleted file mode 100644 index 483b0a174..000000000 --- a/cw_monero/lib/api/exceptions/connection_to_node_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class ConnectionToNodeException implements Exception { - ConnectionToNodeException({required this.message}); - - final String message; -} \ No newline at end of file diff --git a/cw_monero/lib/api/structs/account_row.dart b/cw_monero/lib/api/structs/account_row.dart deleted file mode 100644 index aa492ee0f..000000000 --- a/cw_monero/lib/api/structs/account_row.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class AccountRow extends Struct { - @Int64() - external int id; - - external Pointer label; - - String getLabel() => label.toDartString(); - int getId() => id; -} diff --git a/cw_monero/lib/api/structs/coins_info_row.dart b/cw_monero/lib/api/structs/coins_info_row.dart deleted file mode 100644 index ff6f6ce73..000000000 --- a/cw_monero/lib/api/structs/coins_info_row.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class CoinsInfoRow extends Struct { - @Int64() - external int blockHeight; - - external Pointer hash; - - @Uint64() - external int internalOutputIndex; - - @Uint64() - external int globalOutputIndex; - - @Int8() - external int spent; - - @Int8() - external int frozen; - - @Uint64() - external int spentHeight; - - @Uint64() - external int amount; - - @Int8() - external int rct; - - @Int8() - external int keyImageKnown; - - @Uint64() - external int pkIndex; - - @Uint32() - external int subaddrIndex; - - @Uint32() - external int subaddrAccount; - - external Pointer address; - - external Pointer addressLabel; - - external Pointer keyImage; - - @Uint64() - external int unlockTime; - - @Int8() - external int unlocked; - - external Pointer pubKey; - - @Int8() - external int coinbase; - - external Pointer description; - - String getHash() => hash.toDartString(); - - String getAddress() => address.toDartString(); - - String getAddressLabel() => addressLabel.toDartString(); - - String getKeyImage() => keyImage.toDartString(); - - String getPubKey() => pubKey.toDartString(); - - String getDescription() => description.toDartString(); -} diff --git a/cw_monero/lib/api/structs/subaddress_row.dart b/cw_monero/lib/api/structs/subaddress_row.dart deleted file mode 100644 index d593a793d..000000000 --- a/cw_monero/lib/api/structs/subaddress_row.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class SubaddressRow extends Struct { - @Int64() - external int id; - - external Pointer address; - - external Pointer label; - - String getLabel() => label.toDartString(); - String getAddress() => address.toDartString(); - int getId() => id; -} \ No newline at end of file diff --git a/cw_monero/lib/api/structs/transaction_info_row.dart b/cw_monero/lib/api/structs/transaction_info_row.dart deleted file mode 100644 index bdcc64d3f..000000000 --- a/cw_monero/lib/api/structs/transaction_info_row.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class TransactionInfoRow extends Struct { - @Uint64() - external int amount; - - @Uint64() - external int fee; - - @Uint64() - external int blockHeight; - - @Uint64() - external int confirmations; - - @Uint32() - external int subaddrAccount; - - @Int8() - external int direction; - - @Int8() - external int isPending; - - @Uint32() - external int subaddrIndex; - - external Pointer hash; - - external Pointer paymentId; - - @Int64() - external int datetime; - - int getDatetime() => datetime; - int getAmount() => amount >= 0 ? amount : amount * -1; - bool getIsPending() => isPending != 0; - String getHash() => hash.toDartString(); - String getPaymentId() => paymentId.toDartString(); -} diff --git a/cw_monero/lib/api/structs/ut8_box.dart b/cw_monero/lib/api/structs/ut8_box.dart deleted file mode 100644 index 53e678c88..000000000 --- a/cw_monero/lib/api/structs/ut8_box.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class Utf8Box extends Struct { - external Pointer value; - - String getValue() => value.toDartString(); -} diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index c28f162be..b416e1b4e 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -1,4 +1,3 @@ - import 'dart:ffi'; import 'dart:isolate'; @@ -288,7 +287,7 @@ class Transaction { }; } - // S finalubAddress? subAddress; + // final SubAddress? subAddress; // List transfers = []; // final int txIndex; final monero.TransactionInfo txInfo; @@ -324,4 +323,4 @@ class Transaction { required this.key, required this.txInfo }); -} \ No newline at end of file +} diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 50ab41e04..26c83b06e 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -1,4 +1,5 @@ import 'dart:ffi'; +import 'dart:io'; import 'dart:isolate'; import 'package:cw_monero/api/account_list.dart'; @@ -8,8 +9,42 @@ import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; +import 'package:flutter/foundation.dart'; import 'package:monero/monero.dart' as monero; +class MoneroCException implements Exception { + final String message; + + MoneroCException(this.message); + + @override + String toString() { + return message; + } +} + +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 dartCsCpp = monero.wallet2_api_c_cpp_sha256; + final dartCsH = monero.wallet2_api_c_h_sha256; + final dartCsExp = monero.wallet2_api_c_exp_sha256; + + if (cppCsCpp != dartCsCpp) { + throw MoneroCException("monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'"); + } + + if (cppCsH != dartCsH) { + throw MoneroCException("monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'"); + } + + if (cppCsExp != dartCsExp && (Platform.isIOS || Platform.isMacOS)) { + 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((() { try { diff --git a/cw_monero/lib/cw_monero.dart b/cw_monero/lib/cw_monero.dart deleted file mode 100644 index 7945a020e..000000000 --- a/cw_monero/lib/cw_monero.dart +++ /dev/null @@ -1,8 +0,0 @@ - -import 'cw_monero_platform_interface.dart'; - -class CwMonero { - Future getPlatformVersion() { - return CwMoneroPlatform.instance.getPlatformVersion(); - } -} diff --git a/cw_monero/lib/cw_monero_method_channel.dart b/cw_monero/lib/cw_monero_method_channel.dart deleted file mode 100644 index 1cbca9f2c..000000000 --- a/cw_monero/lib/cw_monero_method_channel.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'cw_monero_platform_interface.dart'; - -/// An implementation of [CwMoneroPlatform] that uses method channels. -class MethodChannelCwMonero extends CwMoneroPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - final methodChannel = const MethodChannel('cw_monero'); - - @override - Future getPlatformVersion() async { - final version = await methodChannel.invokeMethod('getPlatformVersion'); - return version; - } -} diff --git a/cw_monero/lib/cw_monero_platform_interface.dart b/cw_monero/lib/cw_monero_platform_interface.dart deleted file mode 100644 index 6c9b20a25..000000000 --- a/cw_monero/lib/cw_monero_platform_interface.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'cw_monero_method_channel.dart'; - -abstract class CwMoneroPlatform extends PlatformInterface { - /// Constructs a CwMoneroPlatform. - CwMoneroPlatform() : super(token: _token); - - static final Object _token = Object(); - - static CwMoneroPlatform _instance = MethodChannelCwMonero(); - - /// The default instance of [CwMoneroPlatform] to use. - /// - /// Defaults to [MethodChannelCwMonero]. - static CwMoneroPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [CwMoneroPlatform] when - /// they register themselves. - static set instance(CwMoneroPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } -} diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 596b26812..76064ad11 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -1,8 +1,5 @@ -import 'dart:math'; - import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_core/parseBoolFromString.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/format_amount.dart'; @@ -37,26 +34,6 @@ class MoneroTransactionInfo extends TransactionInfo { }; } - MoneroTransactionInfo.fromRow(TransactionInfoRow row) - : id = "${row.getHash()}_${row.getAmount()}_${row.subaddrAccount}_${row.subaddrIndex}", - txHash = row.getHash(), - height = row.blockHeight, - direction = parseTransactionDirectionFromInt(row.direction), - date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), - isPending = row.isPending != 0, - amount = row.getAmount(), - accountIndex = row.subaddrAccount, - addressIndex = row.subaddrIndex, - confirmations = row.confirmations, - key = getTxKey(row.getHash()), - fee = row.fee { - additionalInfo = { - 'key': key, - 'accountIndex': accountIndex, - 'addressIndex': addressIndex - }; - } - final String id; final String txHash; final int height; diff --git a/cw_monero/lib/monero_unspent.dart b/cw_monero/lib/monero_unspent.dart index 65b5c595d..87d8f0b39 100644 --- a/cw_monero/lib/monero_unspent.dart +++ b/cw_monero/lib/monero_unspent.dart @@ -1,5 +1,4 @@ import 'package:cw_core/unspent_transaction_output.dart'; -import 'package:cw_monero/api/structs/coins_info_row.dart'; class MoneroUnspent extends Unspent { MoneroUnspent( @@ -8,13 +7,5 @@ class MoneroUnspent extends Unspent { this.isFrozen = isFrozen; } - factory MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) => MoneroUnspent( - coinsInfoRow.getAddress(), - coinsInfoRow.getHash(), - coinsInfoRow.getKeyImage(), - coinsInfoRow.amount, - coinsInfoRow.frozen == 1, - coinsInfoRow.unlocked == 1); - final bool isUnlocked; } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index b8e3c2765..9298f8a49 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -109,9 +109,7 @@ abstract class MoneroWalletBase extends WalletBase monero_wallet.getSeed(); - String seedLegacy(String? language) { - return monero_wallet.getSeedLegacy(language); - } + String seedLegacy(String? language) => monero_wallet.getSeedLegacy(language); @override MoneroWalletKeys get keys => MoneroWalletKeys( @@ -190,12 +188,12 @@ abstract class MoneroWalletBase extends WalletBase startSync() async { try { - _setInitialHeight(); + _assertInitialHeight(); } catch (_) { // our restore height wasn't correct, so lets see if using the backup works: try { - await resetCache(name); - _setInitialHeight(); + await resetCache(name); // Resetting the cache removes the TX Keys and Polyseed + _assertInitialHeight(); } catch (e) { // we still couldn't get a valid height from the backup?!: // try to use the date instead: @@ -635,18 +633,14 @@ abstract class MoneroWalletBase extends WalletBase MIN_RESTORE_HEIGHT) { - // the restore height is probably correct, so we do nothing: - return; - } + // the restore height is probably correct, so we do nothing: + if (height > MIN_RESTORE_HEIGHT) return; throw Exception("height isn't > $MIN_RESTORE_HEIGHT!"); } diff --git a/cw_monero/lib/mymonero.dart b/cw_monero/lib/mymonero.dart deleted file mode 100644 index d50e48b64..000000000 --- a/cw_monero/lib/mymonero.dart +++ /dev/null @@ -1,1689 +0,0 @@ -const prefixLength = 3; - -String swapEndianBytes(String original) { - if (original.length != 8) { - return ''; - } - - return original[6] + - original[7] + - original[4] + - original[5] + - original[2] + - original[3] + - original[0] + - original[1]; -} - -List tructWords(List wordSet) { - final start = 0; - final end = prefixLength; - - return wordSet.map((word) => word.substring(start, end)).toList(); -} - -String mnemonicDecode(String seed) { - final n = englistWordSet.length; - var out = ''; - var wlist = seed.split(' '); - wlist.removeLast(); - - for (var i = 0; i < wlist.length; i += 3) { - final w1 = - tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength)); - final w2 = tructWords(englistWordSet) - .indexOf(wlist[i + 1].substring(0, prefixLength)); - final w3 = tructWords(englistWordSet) - .indexOf(wlist[i + 2].substring(0, prefixLength)); - - if (w1 == -1 || w2 == -1 || w3 == -1) { - print("invalid word in mnemonic"); - return ''; - } - - final x = w1 + n * (((n - w1) + w2) % n) + n * n * (((n - w2) + w3) % n); - - if (x % n != w1) { - print("Something went wrong when decoding your private key, please try again"); - return ''; - } - - final _res = '0000000' + x.toRadixString(16); - final start = _res.length - 8; - final end = _res.length; - final res = _res.substring(start, end); - - out += swapEndianBytes(res); - } - - return out; -} - -final englistWordSet = [ - "abbey", - "abducts", - "ability", - "ablaze", - "abnormal", - "abort", - "abrasive", - "absorb", - "abyss", - "academy", - "aces", - "aching", - "acidic", - "acoustic", - "acquire", - "across", - "actress", - "acumen", - "adapt", - "addicted", - "adept", - "adhesive", - "adjust", - "adopt", - "adrenalin", - "adult", - "adventure", - "aerial", - "afar", - "affair", - "afield", - "afloat", - "afoot", - "afraid", - "after", - "against", - "agenda", - "aggravate", - "agile", - "aglow", - "agnostic", - "agony", - "agreed", - "ahead", - "aided", - "ailments", - "aimless", - "airport", - "aisle", - "ajar", - "akin", - "alarms", - "album", - "alchemy", - "alerts", - "algebra", - "alkaline", - "alley", - "almost", - "aloof", - "alpine", - "already", - "also", - "altitude", - "alumni", - "always", - "amaze", - "ambush", - "amended", - "amidst", - "ammo", - "amnesty", - "among", - "amply", - "amused", - "anchor", - "android", - "anecdote", - "angled", - "ankle", - "annoyed", - "answers", - "antics", - "anvil", - "anxiety", - "anybody", - "apart", - "apex", - "aphid", - "aplomb", - "apology", - "apply", - "apricot", - "aptitude", - "aquarium", - "arbitrary", - "archer", - "ardent", - "arena", - "argue", - "arises", - "army", - "around", - "arrow", - "arsenic", - "artistic", - "ascend", - "ashtray", - "aside", - "asked", - "asleep", - "aspire", - "assorted", - "asylum", - "athlete", - "atlas", - "atom", - "atrium", - "attire", - "auburn", - "auctions", - "audio", - "august", - "aunt", - "austere", - "autumn", - "avatar", - "avidly", - "avoid", - "awakened", - "awesome", - "awful", - "awkward", - "awning", - "awoken", - "axes", - "axis", - "axle", - "aztec", - "azure", - "baby", - "bacon", - "badge", - "baffles", - "bagpipe", - "bailed", - "bakery", - "balding", - "bamboo", - "banjo", - "baptism", - "basin", - "batch", - "bawled", - "bays", - "because", - "beer", - "befit", - "begun", - "behind", - "being", - "below", - "bemused", - "benches", - "berries", - "bested", - "betting", - "bevel", - "beware", - "beyond", - "bias", - "bicycle", - "bids", - "bifocals", - "biggest", - "bikini", - "bimonthly", - "binocular", - "biology", - "biplane", - "birth", - "biscuit", - "bite", - "biweekly", - "blender", - "blip", - "bluntly", - "boat", - "bobsled", - "bodies", - "bogeys", - "boil", - "boldly", - "bomb", - "border", - "boss", - "both", - "bounced", - "bovine", - "bowling", - "boxes", - "boyfriend", - "broken", - "brunt", - "bubble", - "buckets", - "budget", - "buffet", - "bugs", - "building", - "bulb", - "bumper", - "bunch", - "business", - "butter", - "buying", - "buzzer", - "bygones", - "byline", - "bypass", - "cabin", - "cactus", - "cadets", - "cafe", - "cage", - "cajun", - "cake", - "calamity", - "camp", - "candy", - "casket", - "catch", - "cause", - "cavernous", - "cease", - "cedar", - "ceiling", - "cell", - "cement", - "cent", - "certain", - "chlorine", - "chrome", - "cider", - "cigar", - "cinema", - "circle", - "cistern", - "citadel", - "civilian", - "claim", - "click", - "clue", - "coal", - "cobra", - "cocoa", - "code", - "coexist", - "coffee", - "cogs", - "cohesive", - "coils", - "colony", - "comb", - "cool", - "copy", - "corrode", - "costume", - "cottage", - "cousin", - "cowl", - "criminal", - "cube", - "cucumber", - "cuddled", - "cuffs", - "cuisine", - "cunning", - "cupcake", - "custom", - "cycling", - "cylinder", - "cynical", - "dabbing", - "dads", - "daft", - "dagger", - "daily", - "damp", - "dangerous", - "dapper", - "darted", - "dash", - "dating", - "dauntless", - "dawn", - "daytime", - "dazed", - "debut", - "decay", - "dedicated", - "deepest", - "deftly", - "degrees", - "dehydrate", - "deity", - "dejected", - "delayed", - "demonstrate", - "dented", - "deodorant", - "depth", - "desk", - "devoid", - "dewdrop", - "dexterity", - "dialect", - "dice", - "diet", - "different", - "digit", - "dilute", - "dime", - "dinner", - "diode", - "diplomat", - "directed", - "distance", - "ditch", - "divers", - "dizzy", - "doctor", - "dodge", - "does", - "dogs", - "doing", - "dolphin", - "domestic", - "donuts", - "doorway", - "dormant", - "dosage", - "dotted", - "double", - "dove", - "down", - "dozen", - "dreams", - "drinks", - "drowning", - "drunk", - "drying", - "dual", - "dubbed", - "duckling", - "dude", - "duets", - "duke", - "dullness", - "dummy", - "dunes", - "duplex", - "duration", - "dusted", - "duties", - "dwarf", - "dwelt", - "dwindling", - "dying", - "dynamite", - "dyslexic", - "each", - "eagle", - "earth", - "easy", - "eating", - "eavesdrop", - "eccentric", - "echo", - "eclipse", - "economics", - "ecstatic", - "eden", - "edgy", - "edited", - "educated", - "eels", - "efficient", - "eggs", - "egotistic", - "eight", - "either", - "eject", - "elapse", - "elbow", - "eldest", - "eleven", - "elite", - "elope", - "else", - "eluded", - "emails", - "ember", - "emerge", - "emit", - "emotion", - "empty", - "emulate", - "energy", - "enforce", - "enhanced", - "enigma", - "enjoy", - "enlist", - "enmity", - "enough", - "enraged", - "ensign", - "entrance", - "envy", - "epoxy", - "equip", - "erase", - "erected", - "erosion", - "error", - "eskimos", - "espionage", - "essential", - "estate", - "etched", - "eternal", - "ethics", - "etiquette", - "evaluate", - "evenings", - "evicted", - "evolved", - "examine", - "excess", - "exhale", - "exit", - "exotic", - "exquisite", - "extra", - "exult", - "fabrics", - "factual", - "fading", - "fainted", - "faked", - "fall", - "family", - "fancy", - "farming", - "fatal", - "faulty", - "fawns", - "faxed", - "fazed", - "feast", - "february", - "federal", - "feel", - "feline", - "females", - "fences", - "ferry", - "festival", - "fetches", - "fever", - "fewest", - "fiat", - "fibula", - "fictional", - "fidget", - "fierce", - "fifteen", - "fight", - "films", - "firm", - "fishing", - "fitting", - "five", - "fixate", - "fizzle", - "fleet", - "flippant", - "flying", - "foamy", - "focus", - "foes", - "foggy", - "foiled", - "folding", - "fonts", - "foolish", - "fossil", - "fountain", - "fowls", - "foxes", - "foyer", - "framed", - "friendly", - "frown", - "fruit", - "frying", - "fudge", - "fuel", - "fugitive", - "fully", - "fuming", - "fungal", - "furnished", - "fuselage", - "future", - "fuzzy", - "gables", - "gadget", - "gags", - "gained", - "galaxy", - "gambit", - "gang", - "gasp", - "gather", - "gauze", - "gave", - "gawk", - "gaze", - "gearbox", - "gecko", - "geek", - "gels", - "gemstone", - "general", - "geometry", - "germs", - "gesture", - "getting", - "geyser", - "ghetto", - "ghost", - "giant", - "giddy", - "gifts", - "gigantic", - "gills", - "gimmick", - "ginger", - "girth", - "giving", - "glass", - "gleeful", - "glide", - "gnaw", - "gnome", - "goat", - "goblet", - "godfather", - "goes", - "goggles", - "going", - "goldfish", - "gone", - "goodbye", - "gopher", - "gorilla", - "gossip", - "gotten", - "gourmet", - "governing", - "gown", - "greater", - "grunt", - "guarded", - "guest", - "guide", - "gulp", - "gumball", - "guru", - "gusts", - "gutter", - "guys", - "gymnast", - "gypsy", - "gyrate", - "habitat", - "hacksaw", - "haggled", - "hairy", - "hamburger", - "happens", - "hashing", - "hatchet", - "haunted", - "having", - "hawk", - "haystack", - "hazard", - "hectare", - "hedgehog", - "heels", - "hefty", - "height", - "hemlock", - "hence", - "heron", - "hesitate", - "hexagon", - "hickory", - "hiding", - "highway", - "hijack", - "hiker", - "hills", - "himself", - "hinder", - "hippo", - "hire", - "history", - "hitched", - "hive", - "hoax", - "hobby", - "hockey", - "hoisting", - "hold", - "honked", - "hookup", - "hope", - "hornet", - "hospital", - "hotel", - "hounded", - "hover", - "howls", - "hubcaps", - "huddle", - "huge", - "hull", - "humid", - "hunter", - "hurried", - "husband", - "huts", - "hybrid", - "hydrogen", - "hyper", - "iceberg", - "icing", - "icon", - "identity", - "idiom", - "idled", - "idols", - "igloo", - "ignore", - "iguana", - "illness", - "imagine", - "imbalance", - "imitate", - "impel", - "inactive", - "inbound", - "incur", - "industrial", - "inexact", - "inflamed", - "ingested", - "initiate", - "injury", - "inkling", - "inline", - "inmate", - "innocent", - "inorganic", - "input", - "inquest", - "inroads", - "insult", - "intended", - "inundate", - "invoke", - "inwardly", - "ionic", - "irate", - "iris", - "irony", - "irritate", - "island", - "isolated", - "issued", - "italics", - "itches", - "items", - "itinerary", - "itself", - "ivory", - "jabbed", - "jackets", - "jaded", - "jagged", - "jailed", - "jamming", - "january", - "jargon", - "jaunt", - "javelin", - "jaws", - "jazz", - "jeans", - "jeers", - "jellyfish", - "jeopardy", - "jerseys", - "jester", - "jetting", - "jewels", - "jigsaw", - "jingle", - "jittery", - "jive", - "jobs", - "jockey", - "jogger", - "joining", - "joking", - "jolted", - "jostle", - "journal", - "joyous", - "jubilee", - "judge", - "juggled", - "juicy", - "jukebox", - "july", - "jump", - "junk", - "jury", - "justice", - "juvenile", - "kangaroo", - "karate", - "keep", - "kennel", - "kept", - "kernels", - "kettle", - "keyboard", - "kickoff", - "kidneys", - "king", - "kiosk", - "kisses", - "kitchens", - "kiwi", - "knapsack", - "knee", - "knife", - "knowledge", - "knuckle", - "koala", - "laboratory", - "ladder", - "lagoon", - "lair", - "lakes", - "lamb", - "language", - "laptop", - "large", - "last", - "later", - "launching", - "lava", - "lawsuit", - "layout", - "lazy", - "lectures", - "ledge", - "leech", - "left", - "legion", - "leisure", - "lemon", - "lending", - "leopard", - "lesson", - "lettuce", - "lexicon", - "liar", - "library", - "licks", - "lids", - "lied", - "lifestyle", - "light", - "likewise", - "lilac", - "limits", - "linen", - "lion", - "lipstick", - "liquid", - "listen", - "lively", - "loaded", - "lobster", - "locker", - "lodge", - "lofty", - "logic", - "loincloth", - "long", - "looking", - "lopped", - "lordship", - "losing", - "lottery", - "loudly", - "love", - "lower", - "loyal", - "lucky", - "luggage", - "lukewarm", - "lullaby", - "lumber", - "lunar", - "lurk", - "lush", - "luxury", - "lymph", - "lynx", - "lyrics", - "macro", - "madness", - "magically", - "mailed", - "major", - "makeup", - "malady", - "mammal", - "maps", - "masterful", - "match", - "maul", - "maverick", - "maximum", - "mayor", - "maze", - "meant", - "mechanic", - "medicate", - "meeting", - "megabyte", - "melting", - "memoir", - "menu", - "merger", - "mesh", - "metro", - "mews", - "mice", - "midst", - "mighty", - "mime", - "mirror", - "misery", - "mittens", - "mixture", - "moat", - "mobile", - "mocked", - "mohawk", - "moisture", - "molten", - "moment", - "money", - "moon", - "mops", - "morsel", - "mostly", - "motherly", - "mouth", - "movement", - "mowing", - "much", - "muddy", - "muffin", - "mugged", - "mullet", - "mumble", - "mundane", - "muppet", - "mural", - "musical", - "muzzle", - "myriad", - "mystery", - "myth", - "nabbing", - "nagged", - "nail", - "names", - "nanny", - "napkin", - "narrate", - "nasty", - "natural", - "nautical", - "navy", - "nearby", - "necklace", - "needed", - "negative", - "neither", - "neon", - "nephew", - "nerves", - "nestle", - "network", - "neutral", - "never", - "newt", - "nexus", - "nibs", - "niche", - "niece", - "nifty", - "nightly", - "nimbly", - "nineteen", - "nirvana", - "nitrogen", - "nobody", - "nocturnal", - "nodes", - "noises", - "nomad", - "noodles", - "northern", - "nostril", - "noted", - "nouns", - "novelty", - "nowhere", - "nozzle", - "nuance", - "nucleus", - "nudged", - "nugget", - "nuisance", - "null", - "number", - "nuns", - "nurse", - "nutshell", - "nylon", - "oaks", - "oars", - "oasis", - "oatmeal", - "obedient", - "object", - "obliged", - "obnoxious", - "observant", - "obtains", - "obvious", - "occur", - "ocean", - "october", - "odds", - "odometer", - "offend", - "often", - "oilfield", - "ointment", - "okay", - "older", - "olive", - "olympics", - "omega", - "omission", - "omnibus", - "onboard", - "oncoming", - "oneself", - "ongoing", - "onion", - "online", - "onslaught", - "onto", - "onward", - "oozed", - "opacity", - "opened", - "opposite", - "optical", - "opus", - "orange", - "orbit", - "orchid", - "orders", - "organs", - "origin", - "ornament", - "orphans", - "oscar", - "ostrich", - "otherwise", - "otter", - "ouch", - "ought", - "ounce", - "ourselves", - "oust", - "outbreak", - "oval", - "oven", - "owed", - "owls", - "owner", - "oxidant", - "oxygen", - "oyster", - "ozone", - "pact", - "paddles", - "pager", - "pairing", - "palace", - "pamphlet", - "pancakes", - "paper", - "paradise", - "pastry", - "patio", - "pause", - "pavements", - "pawnshop", - "payment", - "peaches", - "pebbles", - "peculiar", - "pedantic", - "peeled", - "pegs", - "pelican", - "pencil", - "people", - "pepper", - "perfect", - "pests", - "petals", - "phase", - "pheasants", - "phone", - "phrases", - "physics", - "piano", - "picked", - "pierce", - "pigment", - "piloted", - "pimple", - "pinched", - "pioneer", - "pipeline", - "pirate", - "pistons", - "pitched", - "pivot", - "pixels", - "pizza", - "playful", - "pledge", - "pliers", - "plotting", - "plus", - "plywood", - "poaching", - "pockets", - "podcast", - "poetry", - "point", - "poker", - "polar", - "ponies", - "pool", - "popular", - "portents", - "possible", - "potato", - "pouch", - "poverty", - "powder", - "pram", - "present", - "pride", - "problems", - "pruned", - "prying", - "psychic", - "public", - "puck", - "puddle", - "puffin", - "pulp", - "pumpkins", - "punch", - "puppy", - "purged", - "push", - "putty", - "puzzled", - "pylons", - "pyramid", - "python", - "queen", - "quick", - "quote", - "rabbits", - "racetrack", - "radar", - "rafts", - "rage", - "railway", - "raking", - "rally", - "ramped", - "randomly", - "rapid", - "rarest", - "rash", - "rated", - "ravine", - "rays", - "razor", - "react", - "rebel", - "recipe", - "reduce", - "reef", - "refer", - "regular", - "reheat", - "reinvest", - "rejoices", - "rekindle", - "relic", - "remedy", - "renting", - "reorder", - "repent", - "request", - "reruns", - "rest", - "return", - "reunion", - "revamp", - "rewind", - "rhino", - "rhythm", - "ribbon", - "richly", - "ridges", - "rift", - "rigid", - "rims", - "ringing", - "riots", - "ripped", - "rising", - "ritual", - "river", - "roared", - "robot", - "rockets", - "rodent", - "rogue", - "roles", - "romance", - "roomy", - "roped", - "roster", - "rotate", - "rounded", - "rover", - "rowboat", - "royal", - "ruby", - "rudely", - "ruffled", - "rugged", - "ruined", - "ruling", - "rumble", - "runway", - "rural", - "rustled", - "ruthless", - "sabotage", - "sack", - "sadness", - "safety", - "saga", - "sailor", - "sake", - "salads", - "sample", - "sanity", - "sapling", - "sarcasm", - "sash", - "satin", - "saucepan", - "saved", - "sawmill", - "saxophone", - "sayings", - "scamper", - "scenic", - "school", - "science", - "scoop", - "scrub", - "scuba", - "seasons", - "second", - "sedan", - "seeded", - "segments", - "seismic", - "selfish", - "semifinal", - "sensible", - "september", - "sequence", - "serving", - "session", - "setup", - "seventh", - "sewage", - "shackles", - "shelter", - "shipped", - "shocking", - "shrugged", - "shuffled", - "shyness", - "siblings", - "sickness", - "sidekick", - "sieve", - "sifting", - "sighting", - "silk", - "simplest", - "sincerely", - "sipped", - "siren", - "situated", - "sixteen", - "sizes", - "skater", - "skew", - "skirting", - "skulls", - "skydive", - "slackens", - "sleepless", - "slid", - "slower", - "slug", - "smash", - "smelting", - "smidgen", - "smog", - "smuggled", - "snake", - "sneeze", - "sniff", - "snout", - "snug", - "soapy", - "sober", - "soccer", - "soda", - "software", - "soggy", - "soil", - "solved", - "somewhere", - "sonic", - "soothe", - "soprano", - "sorry", - "southern", - "sovereign", - "sowed", - "soya", - "space", - "speedy", - "sphere", - "spiders", - "splendid", - "spout", - "sprig", - "spud", - "spying", - "square", - "stacking", - "stellar", - "stick", - "stockpile", - "strained", - "stunning", - "stylishly", - "subtly", - "succeed", - "suddenly", - "suede", - "suffice", - "sugar", - "suitcase", - "sulking", - "summon", - "sunken", - "superior", - "surfer", - "sushi", - "suture", - "swagger", - "swept", - "swiftly", - "sword", - "swung", - "syllabus", - "symptoms", - "syndrome", - "syringe", - "system", - "taboo", - "tacit", - "tadpoles", - "tagged", - "tail", - "taken", - "talent", - "tamper", - "tanks", - "tapestry", - "tarnished", - "tasked", - "tattoo", - "taunts", - "tavern", - "tawny", - "taxi", - "teardrop", - "technical", - "tedious", - "teeming", - "tell", - "template", - "tender", - "tepid", - "tequila", - "terminal", - "testing", - "tether", - "textbook", - "thaw", - "theatrics", - "thirsty", - "thorn", - "threaten", - "thumbs", - "thwart", - "ticket", - "tidy", - "tiers", - "tiger", - "tilt", - "timber", - "tinted", - "tipsy", - "tirade", - "tissue", - "titans", - "toaster", - "tobacco", - "today", - "toenail", - "toffee", - "together", - "toilet", - "token", - "tolerant", - "tomorrow", - "tonic", - "toolbox", - "topic", - "torch", - "tossed", - "total", - "touchy", - "towel", - "toxic", - "toyed", - "trash", - "trendy", - "tribal", - "trolling", - "truth", - "trying", - "tsunami", - "tubes", - "tucks", - "tudor", - "tuesday", - "tufts", - "tugs", - "tuition", - "tulips", - "tumbling", - "tunnel", - "turnip", - "tusks", - "tutor", - "tuxedo", - "twang", - "tweezers", - "twice", - "twofold", - "tycoon", - "typist", - "tyrant", - "ugly", - "ulcers", - "ultimate", - "umbrella", - "umpire", - "unafraid", - "unbending", - "uncle", - "under", - "uneven", - "unfit", - "ungainly", - "unhappy", - "union", - "unjustly", - "unknown", - "unlikely", - "unmask", - "unnoticed", - "unopened", - "unplugs", - "unquoted", - "unrest", - "unsafe", - "until", - "unusual", - "unveil", - "unwind", - "unzip", - "upbeat", - "upcoming", - "update", - "upgrade", - "uphill", - "upkeep", - "upload", - "upon", - "upper", - "upright", - "upstairs", - "uptight", - "upwards", - "urban", - "urchins", - "urgent", - "usage", - "useful", - "usher", - "using", - "usual", - "utensils", - "utility", - "utmost", - "utopia", - "uttered", - "vacation", - "vague", - "vain", - "value", - "vampire", - "vane", - "vapidly", - "vary", - "vastness", - "vats", - "vaults", - "vector", - "veered", - "vegan", - "vehicle", - "vein", - "velvet", - "venomous", - "verification", - "vessel", - "veteran", - "vexed", - "vials", - "vibrate", - "victim", - "video", - "viewpoint", - "vigilant", - "viking", - "village", - "vinegar", - "violin", - "vipers", - "virtual", - "visited", - "vitals", - "vivid", - "vixen", - "vocal", - "vogue", - "voice", - "volcano", - "vortex", - "voted", - "voucher", - "vowels", - "voyage", - "vulture", - "wade", - "waffle", - "wagtail", - "waist", - "waking", - "wallets", - "wanted", - "warped", - "washing", - "water", - "waveform", - "waxing", - "wayside", - "weavers", - "website", - "wedge", - "weekday", - "weird", - "welders", - "went", - "wept", - "were", - "western", - "wetsuit", - "whale", - "when", - "whipped", - "whole", - "wickets", - "width", - "wield", - "wife", - "wiggle", - "wildly", - "winter", - "wipeout", - "wiring", - "wise", - "withdrawn", - "wives", - "wizard", - "wobbly", - "woes", - "woken", - "wolf", - "womanly", - "wonders", - "woozy", - "worry", - "wounded", - "woven", - "wrap", - "wrist", - "wrong", - "yacht", - "yahoo", - "yanks", - "yard", - "yawning", - "yearbook", - "yellow", - "yesterday", - "yeti", - "yields", - "yodel", - "yoga", - "younger", - "yoyo", - "zapped", - "zeal", - "zebra", - "zero", - "zesty", - "zigzags", - "zinger", - "zippers", - "zodiac", - "zombie", - "zones", - "zoom" -]; diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 011fed169..838f7224c 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.3" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: "direct dev" description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.10" built_collection: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.9.2" characters: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" clock: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.10.0" collection: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" cw_core: dependency: "direct main" description: @@ -188,10 +188,10 @@ packages: dependency: "direct main" description: name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.3" fake_async: dependency: transitive description: @@ -204,10 +204,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -233,10 +233,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" url: "https://pub.dev" source: hosted - version: "2.0.6+5" + version: "2.2.1+1" flutter_test: dependency: "direct dev" description: flutter @@ -246,42 +246,42 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" hashlib: dependency: transitive description: name: hashlib - sha256: "71bf102329ddb8e50c8a995ee4645ae7f1728bb65e575c17196b4d8262121a96" + sha256: "5037d3b8c36384c03a728543ae67d962a56970c5432a50862279fe68ee4c8411" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.19.1" hashlib_codecs: dependency: transitive description: name: hashlib_codecs - sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626" + sha256: "2b570061f5a4b378425be28a576c1e11783450355ad4345a19f606ff3d96db0f" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.5.0" hive: dependency: transitive description: @@ -302,10 +302,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -342,18 +342,18 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json_annotation: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -382,10 +382,10 @@ packages: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" matcher: dependency: transitive description: @@ -414,33 +414,33 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mobx: dependency: "direct main" description: name: mobx - sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a + sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" url: "https://pub.dev" source: hosted - version: "2.1.3+1" + version: "2.3.3+2" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181" + sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.0" monero: dependency: "direct main" description: - path: "." - ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 - resolved-ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 - url: "https://github.com/mrcyjanek/monero.dart" + path: "impls/monero.dart" + ref: "bcb328a4956105dc182afd0ce2e48fe263f5f20b" + resolved-ref: "bcb328a4956105dc182afd0ce2e48fe263f5f20b" + url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" mutex: @@ -451,6 +451,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -471,26 +479,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.4" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -503,10 +511,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -519,26 +527,26 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.9.1" polyseed: dependency: "direct main" description: @@ -555,46 +563,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: + provider: dependency: transitive description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "6.1.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" shelf: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -604,10 +612,10 @@ packages: dependency: transitive description: name: socks5_proxy - sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.6" source_gen: dependency: transitive description: @@ -692,10 +700,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unorm_dart: dependency: transitive description: @@ -728,38 +736,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.5" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "5.5.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.16.6" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 53e50877f..b5a13a126 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -24,8 +24,9 @@ dependencies: path: ../cw_core monero: git: - url: https://github.com/mrcyjanek/monero.dart - ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 + url: https://github.com/mrcyjanek/monero_c + ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b # monero_c hash + path: impls/monero.dart mutex: ^3.1.0 dev_dependencies: diff --git a/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart b/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart deleted file mode 100644 index 483b0a174..000000000 --- a/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class ConnectionToNodeException implements Exception { - ConnectionToNodeException({required this.message}); - - final String message; -} \ No newline at end of file diff --git a/cw_wownero/lib/api/structs/account_row.dart b/cw_wownero/lib/api/structs/account_row.dart deleted file mode 100644 index aa492ee0f..000000000 --- a/cw_wownero/lib/api/structs/account_row.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class AccountRow extends Struct { - @Int64() - external int id; - - external Pointer label; - - String getLabel() => label.toDartString(); - int getId() => id; -} diff --git a/cw_wownero/lib/api/structs/coins_info_row.dart b/cw_wownero/lib/api/structs/coins_info_row.dart deleted file mode 100644 index ff6f6ce73..000000000 --- a/cw_wownero/lib/api/structs/coins_info_row.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class CoinsInfoRow extends Struct { - @Int64() - external int blockHeight; - - external Pointer hash; - - @Uint64() - external int internalOutputIndex; - - @Uint64() - external int globalOutputIndex; - - @Int8() - external int spent; - - @Int8() - external int frozen; - - @Uint64() - external int spentHeight; - - @Uint64() - external int amount; - - @Int8() - external int rct; - - @Int8() - external int keyImageKnown; - - @Uint64() - external int pkIndex; - - @Uint32() - external int subaddrIndex; - - @Uint32() - external int subaddrAccount; - - external Pointer address; - - external Pointer addressLabel; - - external Pointer keyImage; - - @Uint64() - external int unlockTime; - - @Int8() - external int unlocked; - - external Pointer pubKey; - - @Int8() - external int coinbase; - - external Pointer description; - - String getHash() => hash.toDartString(); - - String getAddress() => address.toDartString(); - - String getAddressLabel() => addressLabel.toDartString(); - - String getKeyImage() => keyImage.toDartString(); - - String getPubKey() => pubKey.toDartString(); - - String getDescription() => description.toDartString(); -} diff --git a/cw_wownero/lib/api/structs/subaddress_row.dart b/cw_wownero/lib/api/structs/subaddress_row.dart deleted file mode 100644 index d593a793d..000000000 --- a/cw_wownero/lib/api/structs/subaddress_row.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class SubaddressRow extends Struct { - @Int64() - external int id; - - external Pointer address; - - external Pointer label; - - String getLabel() => label.toDartString(); - String getAddress() => address.toDartString(); - int getId() => id; -} \ No newline at end of file diff --git a/cw_wownero/lib/api/structs/transaction_info_row.dart b/cw_wownero/lib/api/structs/transaction_info_row.dart deleted file mode 100644 index bdcc64d3f..000000000 --- a/cw_wownero/lib/api/structs/transaction_info_row.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class TransactionInfoRow extends Struct { - @Uint64() - external int amount; - - @Uint64() - external int fee; - - @Uint64() - external int blockHeight; - - @Uint64() - external int confirmations; - - @Uint32() - external int subaddrAccount; - - @Int8() - external int direction; - - @Int8() - external int isPending; - - @Uint32() - external int subaddrIndex; - - external Pointer hash; - - external Pointer paymentId; - - @Int64() - external int datetime; - - int getDatetime() => datetime; - int getAmount() => amount >= 0 ? amount : amount * -1; - bool getIsPending() => isPending != 0; - String getHash() => hash.toDartString(); - String getPaymentId() => paymentId.toDartString(); -} diff --git a/cw_wownero/lib/api/structs/ut8_box.dart b/cw_wownero/lib/api/structs/ut8_box.dart deleted file mode 100644 index 53e678c88..000000000 --- a/cw_wownero/lib/api/structs/ut8_box.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class Utf8Box extends Struct { - external Pointer value; - - String getValue() => value.toDartString(); -} diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart index 3ccd0b3c6..a1e1e3c9b 100644 --- a/cw_wownero/lib/api/transaction_history.dart +++ b/cw_wownero/lib/api/transaction_history.dart @@ -285,7 +285,7 @@ class Transaction { }; } - // S finalubAddress? subAddress; + // final SubAddress? subAddress; // List transfers = []; // final int txIndex; final wownero.TransactionInfo txInfo; @@ -321,4 +321,4 @@ class Transaction { required this.key, required this.txInfo }); -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/wallet_manager.dart b/cw_wownero/lib/api/wallet_manager.dart index afcc536e7..7915373bb 100644 --- a/cw_wownero/lib/api/wallet_manager.dart +++ b/cw_wownero/lib/api/wallet_manager.dart @@ -1,4 +1,5 @@ import 'dart:ffi'; +import 'dart:io'; import 'dart:isolate'; import 'package:cw_wownero/api/account_list.dart'; @@ -8,8 +9,42 @@ import 'package:cw_wownero/api/exceptions/wallet_restore_from_keys_exception.dar import 'package:cw_wownero/api/exceptions/wallet_restore_from_seed_exception.dart'; import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/wallet.dart'; +import 'package:flutter/foundation.dart'; import 'package:monero/wownero.dart' as wownero; +class MoneroCException implements Exception { + final String message; + + MoneroCException(this.message); + + @override + String toString() { + return message; + } +} + +void checkIfMoneroCIsFine() { + final cppCsCpp = wownero.WOWNERO_checksum_wallet2_api_c_cpp(); + final cppCsH = wownero.WOWNERO_checksum_wallet2_api_c_h(); + final cppCsExp = wownero.WOWNERO_checksum_wallet2_api_c_exp(); + + final dartCsCpp = wownero.wallet2_api_c_cpp_sha256; + final dartCsH = wownero.wallet2_api_c_h_sha256; + final dartCsExp = wownero.wallet2_api_c_exp_sha256; + + if (cppCsCpp != dartCsCpp) { + throw MoneroCException("monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'"); + } + + if (cppCsH != dartCsH) { + throw MoneroCException("monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'"); + } + + if (cppCsExp != dartCsExp && (Platform.isIOS || Platform.isMacOS)) { + 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'"); + } +} + wownero.WalletManager? _wmPtr; final wownero.WalletManager wmPtr = Pointer.fromAddress((() { try { diff --git a/cw_wownero/lib/cw_wownero.dart b/cw_wownero/lib/cw_wownero.dart deleted file mode 100644 index 33a55e305..000000000 --- a/cw_wownero/lib/cw_wownero.dart +++ /dev/null @@ -1,8 +0,0 @@ - -import 'cw_wownero_platform_interface.dart'; - -class CwWownero { - Future getPlatformVersion() { - return CwWowneroPlatform.instance.getPlatformVersion(); - } -} diff --git a/cw_wownero/lib/cw_wownero_method_channel.dart b/cw_wownero/lib/cw_wownero_method_channel.dart deleted file mode 100644 index d797f5f81..000000000 --- a/cw_wownero/lib/cw_wownero_method_channel.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'cw_wownero_platform_interface.dart'; - -/// An implementation of [CwWowneroPlatform] that uses method channels. -class MethodChannelCwWownero extends CwWowneroPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - final methodChannel = const MethodChannel('cw_wownero'); - - @override - Future getPlatformVersion() async { - final version = await methodChannel.invokeMethod('getPlatformVersion'); - return version; - } -} diff --git a/cw_wownero/lib/cw_wownero_platform_interface.dart b/cw_wownero/lib/cw_wownero_platform_interface.dart deleted file mode 100644 index 78b21592c..000000000 --- a/cw_wownero/lib/cw_wownero_platform_interface.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'cw_wownero_method_channel.dart'; - -abstract class CwWowneroPlatform extends PlatformInterface { - /// Constructs a CwWowneroPlatform. - CwWowneroPlatform() : super(token: _token); - - static final Object _token = Object(); - - static CwWowneroPlatform _instance = MethodChannelCwWownero(); - - /// The default instance of [CwWowneroPlatform] to use. - /// - /// Defaults to [MethodChannelCwWownero]. - static CwWowneroPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [CwWowneroPlatform] when - /// they register themselves. - static set instance(CwWowneroPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } -} diff --git a/cw_wownero/lib/mywownero.dart b/cw_wownero/lib/mywownero.dart deleted file mode 100644 index d50e48b64..000000000 --- a/cw_wownero/lib/mywownero.dart +++ /dev/null @@ -1,1689 +0,0 @@ -const prefixLength = 3; - -String swapEndianBytes(String original) { - if (original.length != 8) { - return ''; - } - - return original[6] + - original[7] + - original[4] + - original[5] + - original[2] + - original[3] + - original[0] + - original[1]; -} - -List tructWords(List wordSet) { - final start = 0; - final end = prefixLength; - - return wordSet.map((word) => word.substring(start, end)).toList(); -} - -String mnemonicDecode(String seed) { - final n = englistWordSet.length; - var out = ''; - var wlist = seed.split(' '); - wlist.removeLast(); - - for (var i = 0; i < wlist.length; i += 3) { - final w1 = - tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength)); - final w2 = tructWords(englistWordSet) - .indexOf(wlist[i + 1].substring(0, prefixLength)); - final w3 = tructWords(englistWordSet) - .indexOf(wlist[i + 2].substring(0, prefixLength)); - - if (w1 == -1 || w2 == -1 || w3 == -1) { - print("invalid word in mnemonic"); - return ''; - } - - final x = w1 + n * (((n - w1) + w2) % n) + n * n * (((n - w2) + w3) % n); - - if (x % n != w1) { - print("Something went wrong when decoding your private key, please try again"); - return ''; - } - - final _res = '0000000' + x.toRadixString(16); - final start = _res.length - 8; - final end = _res.length; - final res = _res.substring(start, end); - - out += swapEndianBytes(res); - } - - return out; -} - -final englistWordSet = [ - "abbey", - "abducts", - "ability", - "ablaze", - "abnormal", - "abort", - "abrasive", - "absorb", - "abyss", - "academy", - "aces", - "aching", - "acidic", - "acoustic", - "acquire", - "across", - "actress", - "acumen", - "adapt", - "addicted", - "adept", - "adhesive", - "adjust", - "adopt", - "adrenalin", - "adult", - "adventure", - "aerial", - "afar", - "affair", - "afield", - "afloat", - "afoot", - "afraid", - "after", - "against", - "agenda", - "aggravate", - "agile", - "aglow", - "agnostic", - "agony", - "agreed", - "ahead", - "aided", - "ailments", - "aimless", - "airport", - "aisle", - "ajar", - "akin", - "alarms", - "album", - "alchemy", - "alerts", - "algebra", - "alkaline", - "alley", - "almost", - "aloof", - "alpine", - "already", - "also", - "altitude", - "alumni", - "always", - "amaze", - "ambush", - "amended", - "amidst", - "ammo", - "amnesty", - "among", - "amply", - "amused", - "anchor", - "android", - "anecdote", - "angled", - "ankle", - "annoyed", - "answers", - "antics", - "anvil", - "anxiety", - "anybody", - "apart", - "apex", - "aphid", - "aplomb", - "apology", - "apply", - "apricot", - "aptitude", - "aquarium", - "arbitrary", - "archer", - "ardent", - "arena", - "argue", - "arises", - "army", - "around", - "arrow", - "arsenic", - "artistic", - "ascend", - "ashtray", - "aside", - "asked", - "asleep", - "aspire", - "assorted", - "asylum", - "athlete", - "atlas", - "atom", - "atrium", - "attire", - "auburn", - "auctions", - "audio", - "august", - "aunt", - "austere", - "autumn", - "avatar", - "avidly", - "avoid", - "awakened", - "awesome", - "awful", - "awkward", - "awning", - "awoken", - "axes", - "axis", - "axle", - "aztec", - "azure", - "baby", - "bacon", - "badge", - "baffles", - "bagpipe", - "bailed", - "bakery", - "balding", - "bamboo", - "banjo", - "baptism", - "basin", - "batch", - "bawled", - "bays", - "because", - "beer", - "befit", - "begun", - "behind", - "being", - "below", - "bemused", - "benches", - "berries", - "bested", - "betting", - "bevel", - "beware", - "beyond", - "bias", - "bicycle", - "bids", - "bifocals", - "biggest", - "bikini", - "bimonthly", - "binocular", - "biology", - "biplane", - "birth", - "biscuit", - "bite", - "biweekly", - "blender", - "blip", - "bluntly", - "boat", - "bobsled", - "bodies", - "bogeys", - "boil", - "boldly", - "bomb", - "border", - "boss", - "both", - "bounced", - "bovine", - "bowling", - "boxes", - "boyfriend", - "broken", - "brunt", - "bubble", - "buckets", - "budget", - "buffet", - "bugs", - "building", - "bulb", - "bumper", - "bunch", - "business", - "butter", - "buying", - "buzzer", - "bygones", - "byline", - "bypass", - "cabin", - "cactus", - "cadets", - "cafe", - "cage", - "cajun", - "cake", - "calamity", - "camp", - "candy", - "casket", - "catch", - "cause", - "cavernous", - "cease", - "cedar", - "ceiling", - "cell", - "cement", - "cent", - "certain", - "chlorine", - "chrome", - "cider", - "cigar", - "cinema", - "circle", - "cistern", - "citadel", - "civilian", - "claim", - "click", - "clue", - "coal", - "cobra", - "cocoa", - "code", - "coexist", - "coffee", - "cogs", - "cohesive", - "coils", - "colony", - "comb", - "cool", - "copy", - "corrode", - "costume", - "cottage", - "cousin", - "cowl", - "criminal", - "cube", - "cucumber", - "cuddled", - "cuffs", - "cuisine", - "cunning", - "cupcake", - "custom", - "cycling", - "cylinder", - "cynical", - "dabbing", - "dads", - "daft", - "dagger", - "daily", - "damp", - "dangerous", - "dapper", - "darted", - "dash", - "dating", - "dauntless", - "dawn", - "daytime", - "dazed", - "debut", - "decay", - "dedicated", - "deepest", - "deftly", - "degrees", - "dehydrate", - "deity", - "dejected", - "delayed", - "demonstrate", - "dented", - "deodorant", - "depth", - "desk", - "devoid", - "dewdrop", - "dexterity", - "dialect", - "dice", - "diet", - "different", - "digit", - "dilute", - "dime", - "dinner", - "diode", - "diplomat", - "directed", - "distance", - "ditch", - "divers", - "dizzy", - "doctor", - "dodge", - "does", - "dogs", - "doing", - "dolphin", - "domestic", - "donuts", - "doorway", - "dormant", - "dosage", - "dotted", - "double", - "dove", - "down", - "dozen", - "dreams", - "drinks", - "drowning", - "drunk", - "drying", - "dual", - "dubbed", - "duckling", - "dude", - "duets", - "duke", - "dullness", - "dummy", - "dunes", - "duplex", - "duration", - "dusted", - "duties", - "dwarf", - "dwelt", - "dwindling", - "dying", - "dynamite", - "dyslexic", - "each", - "eagle", - "earth", - "easy", - "eating", - "eavesdrop", - "eccentric", - "echo", - "eclipse", - "economics", - "ecstatic", - "eden", - "edgy", - "edited", - "educated", - "eels", - "efficient", - "eggs", - "egotistic", - "eight", - "either", - "eject", - "elapse", - "elbow", - "eldest", - "eleven", - "elite", - "elope", - "else", - "eluded", - "emails", - "ember", - "emerge", - "emit", - "emotion", - "empty", - "emulate", - "energy", - "enforce", - "enhanced", - "enigma", - "enjoy", - "enlist", - "enmity", - "enough", - "enraged", - "ensign", - "entrance", - "envy", - "epoxy", - "equip", - "erase", - "erected", - "erosion", - "error", - "eskimos", - "espionage", - "essential", - "estate", - "etched", - "eternal", - "ethics", - "etiquette", - "evaluate", - "evenings", - "evicted", - "evolved", - "examine", - "excess", - "exhale", - "exit", - "exotic", - "exquisite", - "extra", - "exult", - "fabrics", - "factual", - "fading", - "fainted", - "faked", - "fall", - "family", - "fancy", - "farming", - "fatal", - "faulty", - "fawns", - "faxed", - "fazed", - "feast", - "february", - "federal", - "feel", - "feline", - "females", - "fences", - "ferry", - "festival", - "fetches", - "fever", - "fewest", - "fiat", - "fibula", - "fictional", - "fidget", - "fierce", - "fifteen", - "fight", - "films", - "firm", - "fishing", - "fitting", - "five", - "fixate", - "fizzle", - "fleet", - "flippant", - "flying", - "foamy", - "focus", - "foes", - "foggy", - "foiled", - "folding", - "fonts", - "foolish", - "fossil", - "fountain", - "fowls", - "foxes", - "foyer", - "framed", - "friendly", - "frown", - "fruit", - "frying", - "fudge", - "fuel", - "fugitive", - "fully", - "fuming", - "fungal", - "furnished", - "fuselage", - "future", - "fuzzy", - "gables", - "gadget", - "gags", - "gained", - "galaxy", - "gambit", - "gang", - "gasp", - "gather", - "gauze", - "gave", - "gawk", - "gaze", - "gearbox", - "gecko", - "geek", - "gels", - "gemstone", - "general", - "geometry", - "germs", - "gesture", - "getting", - "geyser", - "ghetto", - "ghost", - "giant", - "giddy", - "gifts", - "gigantic", - "gills", - "gimmick", - "ginger", - "girth", - "giving", - "glass", - "gleeful", - "glide", - "gnaw", - "gnome", - "goat", - "goblet", - "godfather", - "goes", - "goggles", - "going", - "goldfish", - "gone", - "goodbye", - "gopher", - "gorilla", - "gossip", - "gotten", - "gourmet", - "governing", - "gown", - "greater", - "grunt", - "guarded", - "guest", - "guide", - "gulp", - "gumball", - "guru", - "gusts", - "gutter", - "guys", - "gymnast", - "gypsy", - "gyrate", - "habitat", - "hacksaw", - "haggled", - "hairy", - "hamburger", - "happens", - "hashing", - "hatchet", - "haunted", - "having", - "hawk", - "haystack", - "hazard", - "hectare", - "hedgehog", - "heels", - "hefty", - "height", - "hemlock", - "hence", - "heron", - "hesitate", - "hexagon", - "hickory", - "hiding", - "highway", - "hijack", - "hiker", - "hills", - "himself", - "hinder", - "hippo", - "hire", - "history", - "hitched", - "hive", - "hoax", - "hobby", - "hockey", - "hoisting", - "hold", - "honked", - "hookup", - "hope", - "hornet", - "hospital", - "hotel", - "hounded", - "hover", - "howls", - "hubcaps", - "huddle", - "huge", - "hull", - "humid", - "hunter", - "hurried", - "husband", - "huts", - "hybrid", - "hydrogen", - "hyper", - "iceberg", - "icing", - "icon", - "identity", - "idiom", - "idled", - "idols", - "igloo", - "ignore", - "iguana", - "illness", - "imagine", - "imbalance", - "imitate", - "impel", - "inactive", - "inbound", - "incur", - "industrial", - "inexact", - "inflamed", - "ingested", - "initiate", - "injury", - "inkling", - "inline", - "inmate", - "innocent", - "inorganic", - "input", - "inquest", - "inroads", - "insult", - "intended", - "inundate", - "invoke", - "inwardly", - "ionic", - "irate", - "iris", - "irony", - "irritate", - "island", - "isolated", - "issued", - "italics", - "itches", - "items", - "itinerary", - "itself", - "ivory", - "jabbed", - "jackets", - "jaded", - "jagged", - "jailed", - "jamming", - "january", - "jargon", - "jaunt", - "javelin", - "jaws", - "jazz", - "jeans", - "jeers", - "jellyfish", - "jeopardy", - "jerseys", - "jester", - "jetting", - "jewels", - "jigsaw", - "jingle", - "jittery", - "jive", - "jobs", - "jockey", - "jogger", - "joining", - "joking", - "jolted", - "jostle", - "journal", - "joyous", - "jubilee", - "judge", - "juggled", - "juicy", - "jukebox", - "july", - "jump", - "junk", - "jury", - "justice", - "juvenile", - "kangaroo", - "karate", - "keep", - "kennel", - "kept", - "kernels", - "kettle", - "keyboard", - "kickoff", - "kidneys", - "king", - "kiosk", - "kisses", - "kitchens", - "kiwi", - "knapsack", - "knee", - "knife", - "knowledge", - "knuckle", - "koala", - "laboratory", - "ladder", - "lagoon", - "lair", - "lakes", - "lamb", - "language", - "laptop", - "large", - "last", - "later", - "launching", - "lava", - "lawsuit", - "layout", - "lazy", - "lectures", - "ledge", - "leech", - "left", - "legion", - "leisure", - "lemon", - "lending", - "leopard", - "lesson", - "lettuce", - "lexicon", - "liar", - "library", - "licks", - "lids", - "lied", - "lifestyle", - "light", - "likewise", - "lilac", - "limits", - "linen", - "lion", - "lipstick", - "liquid", - "listen", - "lively", - "loaded", - "lobster", - "locker", - "lodge", - "lofty", - "logic", - "loincloth", - "long", - "looking", - "lopped", - "lordship", - "losing", - "lottery", - "loudly", - "love", - "lower", - "loyal", - "lucky", - "luggage", - "lukewarm", - "lullaby", - "lumber", - "lunar", - "lurk", - "lush", - "luxury", - "lymph", - "lynx", - "lyrics", - "macro", - "madness", - "magically", - "mailed", - "major", - "makeup", - "malady", - "mammal", - "maps", - "masterful", - "match", - "maul", - "maverick", - "maximum", - "mayor", - "maze", - "meant", - "mechanic", - "medicate", - "meeting", - "megabyte", - "melting", - "memoir", - "menu", - "merger", - "mesh", - "metro", - "mews", - "mice", - "midst", - "mighty", - "mime", - "mirror", - "misery", - "mittens", - "mixture", - "moat", - "mobile", - "mocked", - "mohawk", - "moisture", - "molten", - "moment", - "money", - "moon", - "mops", - "morsel", - "mostly", - "motherly", - "mouth", - "movement", - "mowing", - "much", - "muddy", - "muffin", - "mugged", - "mullet", - "mumble", - "mundane", - "muppet", - "mural", - "musical", - "muzzle", - "myriad", - "mystery", - "myth", - "nabbing", - "nagged", - "nail", - "names", - "nanny", - "napkin", - "narrate", - "nasty", - "natural", - "nautical", - "navy", - "nearby", - "necklace", - "needed", - "negative", - "neither", - "neon", - "nephew", - "nerves", - "nestle", - "network", - "neutral", - "never", - "newt", - "nexus", - "nibs", - "niche", - "niece", - "nifty", - "nightly", - "nimbly", - "nineteen", - "nirvana", - "nitrogen", - "nobody", - "nocturnal", - "nodes", - "noises", - "nomad", - "noodles", - "northern", - "nostril", - "noted", - "nouns", - "novelty", - "nowhere", - "nozzle", - "nuance", - "nucleus", - "nudged", - "nugget", - "nuisance", - "null", - "number", - "nuns", - "nurse", - "nutshell", - "nylon", - "oaks", - "oars", - "oasis", - "oatmeal", - "obedient", - "object", - "obliged", - "obnoxious", - "observant", - "obtains", - "obvious", - "occur", - "ocean", - "october", - "odds", - "odometer", - "offend", - "often", - "oilfield", - "ointment", - "okay", - "older", - "olive", - "olympics", - "omega", - "omission", - "omnibus", - "onboard", - "oncoming", - "oneself", - "ongoing", - "onion", - "online", - "onslaught", - "onto", - "onward", - "oozed", - "opacity", - "opened", - "opposite", - "optical", - "opus", - "orange", - "orbit", - "orchid", - "orders", - "organs", - "origin", - "ornament", - "orphans", - "oscar", - "ostrich", - "otherwise", - "otter", - "ouch", - "ought", - "ounce", - "ourselves", - "oust", - "outbreak", - "oval", - "oven", - "owed", - "owls", - "owner", - "oxidant", - "oxygen", - "oyster", - "ozone", - "pact", - "paddles", - "pager", - "pairing", - "palace", - "pamphlet", - "pancakes", - "paper", - "paradise", - "pastry", - "patio", - "pause", - "pavements", - "pawnshop", - "payment", - "peaches", - "pebbles", - "peculiar", - "pedantic", - "peeled", - "pegs", - "pelican", - "pencil", - "people", - "pepper", - "perfect", - "pests", - "petals", - "phase", - "pheasants", - "phone", - "phrases", - "physics", - "piano", - "picked", - "pierce", - "pigment", - "piloted", - "pimple", - "pinched", - "pioneer", - "pipeline", - "pirate", - "pistons", - "pitched", - "pivot", - "pixels", - "pizza", - "playful", - "pledge", - "pliers", - "plotting", - "plus", - "plywood", - "poaching", - "pockets", - "podcast", - "poetry", - "point", - "poker", - "polar", - "ponies", - "pool", - "popular", - "portents", - "possible", - "potato", - "pouch", - "poverty", - "powder", - "pram", - "present", - "pride", - "problems", - "pruned", - "prying", - "psychic", - "public", - "puck", - "puddle", - "puffin", - "pulp", - "pumpkins", - "punch", - "puppy", - "purged", - "push", - "putty", - "puzzled", - "pylons", - "pyramid", - "python", - "queen", - "quick", - "quote", - "rabbits", - "racetrack", - "radar", - "rafts", - "rage", - "railway", - "raking", - "rally", - "ramped", - "randomly", - "rapid", - "rarest", - "rash", - "rated", - "ravine", - "rays", - "razor", - "react", - "rebel", - "recipe", - "reduce", - "reef", - "refer", - "regular", - "reheat", - "reinvest", - "rejoices", - "rekindle", - "relic", - "remedy", - "renting", - "reorder", - "repent", - "request", - "reruns", - "rest", - "return", - "reunion", - "revamp", - "rewind", - "rhino", - "rhythm", - "ribbon", - "richly", - "ridges", - "rift", - "rigid", - "rims", - "ringing", - "riots", - "ripped", - "rising", - "ritual", - "river", - "roared", - "robot", - "rockets", - "rodent", - "rogue", - "roles", - "romance", - "roomy", - "roped", - "roster", - "rotate", - "rounded", - "rover", - "rowboat", - "royal", - "ruby", - "rudely", - "ruffled", - "rugged", - "ruined", - "ruling", - "rumble", - "runway", - "rural", - "rustled", - "ruthless", - "sabotage", - "sack", - "sadness", - "safety", - "saga", - "sailor", - "sake", - "salads", - "sample", - "sanity", - "sapling", - "sarcasm", - "sash", - "satin", - "saucepan", - "saved", - "sawmill", - "saxophone", - "sayings", - "scamper", - "scenic", - "school", - "science", - "scoop", - "scrub", - "scuba", - "seasons", - "second", - "sedan", - "seeded", - "segments", - "seismic", - "selfish", - "semifinal", - "sensible", - "september", - "sequence", - "serving", - "session", - "setup", - "seventh", - "sewage", - "shackles", - "shelter", - "shipped", - "shocking", - "shrugged", - "shuffled", - "shyness", - "siblings", - "sickness", - "sidekick", - "sieve", - "sifting", - "sighting", - "silk", - "simplest", - "sincerely", - "sipped", - "siren", - "situated", - "sixteen", - "sizes", - "skater", - "skew", - "skirting", - "skulls", - "skydive", - "slackens", - "sleepless", - "slid", - "slower", - "slug", - "smash", - "smelting", - "smidgen", - "smog", - "smuggled", - "snake", - "sneeze", - "sniff", - "snout", - "snug", - "soapy", - "sober", - "soccer", - "soda", - "software", - "soggy", - "soil", - "solved", - "somewhere", - "sonic", - "soothe", - "soprano", - "sorry", - "southern", - "sovereign", - "sowed", - "soya", - "space", - "speedy", - "sphere", - "spiders", - "splendid", - "spout", - "sprig", - "spud", - "spying", - "square", - "stacking", - "stellar", - "stick", - "stockpile", - "strained", - "stunning", - "stylishly", - "subtly", - "succeed", - "suddenly", - "suede", - "suffice", - "sugar", - "suitcase", - "sulking", - "summon", - "sunken", - "superior", - "surfer", - "sushi", - "suture", - "swagger", - "swept", - "swiftly", - "sword", - "swung", - "syllabus", - "symptoms", - "syndrome", - "syringe", - "system", - "taboo", - "tacit", - "tadpoles", - "tagged", - "tail", - "taken", - "talent", - "tamper", - "tanks", - "tapestry", - "tarnished", - "tasked", - "tattoo", - "taunts", - "tavern", - "tawny", - "taxi", - "teardrop", - "technical", - "tedious", - "teeming", - "tell", - "template", - "tender", - "tepid", - "tequila", - "terminal", - "testing", - "tether", - "textbook", - "thaw", - "theatrics", - "thirsty", - "thorn", - "threaten", - "thumbs", - "thwart", - "ticket", - "tidy", - "tiers", - "tiger", - "tilt", - "timber", - "tinted", - "tipsy", - "tirade", - "tissue", - "titans", - "toaster", - "tobacco", - "today", - "toenail", - "toffee", - "together", - "toilet", - "token", - "tolerant", - "tomorrow", - "tonic", - "toolbox", - "topic", - "torch", - "tossed", - "total", - "touchy", - "towel", - "toxic", - "toyed", - "trash", - "trendy", - "tribal", - "trolling", - "truth", - "trying", - "tsunami", - "tubes", - "tucks", - "tudor", - "tuesday", - "tufts", - "tugs", - "tuition", - "tulips", - "tumbling", - "tunnel", - "turnip", - "tusks", - "tutor", - "tuxedo", - "twang", - "tweezers", - "twice", - "twofold", - "tycoon", - "typist", - "tyrant", - "ugly", - "ulcers", - "ultimate", - "umbrella", - "umpire", - "unafraid", - "unbending", - "uncle", - "under", - "uneven", - "unfit", - "ungainly", - "unhappy", - "union", - "unjustly", - "unknown", - "unlikely", - "unmask", - "unnoticed", - "unopened", - "unplugs", - "unquoted", - "unrest", - "unsafe", - "until", - "unusual", - "unveil", - "unwind", - "unzip", - "upbeat", - "upcoming", - "update", - "upgrade", - "uphill", - "upkeep", - "upload", - "upon", - "upper", - "upright", - "upstairs", - "uptight", - "upwards", - "urban", - "urchins", - "urgent", - "usage", - "useful", - "usher", - "using", - "usual", - "utensils", - "utility", - "utmost", - "utopia", - "uttered", - "vacation", - "vague", - "vain", - "value", - "vampire", - "vane", - "vapidly", - "vary", - "vastness", - "vats", - "vaults", - "vector", - "veered", - "vegan", - "vehicle", - "vein", - "velvet", - "venomous", - "verification", - "vessel", - "veteran", - "vexed", - "vials", - "vibrate", - "victim", - "video", - "viewpoint", - "vigilant", - "viking", - "village", - "vinegar", - "violin", - "vipers", - "virtual", - "visited", - "vitals", - "vivid", - "vixen", - "vocal", - "vogue", - "voice", - "volcano", - "vortex", - "voted", - "voucher", - "vowels", - "voyage", - "vulture", - "wade", - "waffle", - "wagtail", - "waist", - "waking", - "wallets", - "wanted", - "warped", - "washing", - "water", - "waveform", - "waxing", - "wayside", - "weavers", - "website", - "wedge", - "weekday", - "weird", - "welders", - "went", - "wept", - "were", - "western", - "wetsuit", - "whale", - "when", - "whipped", - "whole", - "wickets", - "width", - "wield", - "wife", - "wiggle", - "wildly", - "winter", - "wipeout", - "wiring", - "wise", - "withdrawn", - "wives", - "wizard", - "wobbly", - "woes", - "woken", - "wolf", - "womanly", - "wonders", - "woozy", - "worry", - "wounded", - "woven", - "wrap", - "wrist", - "wrong", - "yacht", - "yahoo", - "yanks", - "yard", - "yawning", - "yearbook", - "yellow", - "yesterday", - "yeti", - "yields", - "yodel", - "yoga", - "younger", - "yoyo", - "zapped", - "zeal", - "zebra", - "zero", - "zesty", - "zigzags", - "zinger", - "zippers", - "zodiac", - "zombie", - "zones", - "zoom" -]; diff --git a/cw_wownero/lib/wownero_transaction_info.dart b/cw_wownero/lib/wownero_transaction_info.dart index 7b0073452..db5345e5d 100644 --- a/cw_wownero/lib/wownero_transaction_info.dart +++ b/cw_wownero/lib/wownero_transaction_info.dart @@ -1,6 +1,5 @@ import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wownero_amount_format.dart'; -import 'package:cw_wownero/api/structs/transaction_info_row.dart'; import 'package:cw_core/parseBoolFromString.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/format_amount.dart'; @@ -35,26 +34,6 @@ class WowneroTransactionInfo extends TransactionInfo { }; } - WowneroTransactionInfo.fromRow(TransactionInfoRow row) - : id = "${row.getHash()}_${row.getAmount()}_${row.subaddrAccount}_${row.subaddrIndex}", - txHash = row.getHash(), - height = row.blockHeight, - direction = parseTransactionDirectionFromInt(row.direction), - date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), - isPending = row.isPending != 0, - amount = row.getAmount(), - accountIndex = row.subaddrAccount, - addressIndex = row.subaddrIndex, - confirmations = row.confirmations, - key = getTxKey(row.getHash()), - fee = row.fee { - additionalInfo = { - 'key': key, - 'accountIndex': accountIndex, - 'addressIndex': addressIndex - }; - } - final String id; final String txHash; final int height; diff --git a/cw_wownero/lib/wownero_unspent.dart b/cw_wownero/lib/wownero_unspent.dart index a79106886..fdfdfc7a4 100644 --- a/cw_wownero/lib/wownero_unspent.dart +++ b/cw_wownero/lib/wownero_unspent.dart @@ -1,5 +1,4 @@ import 'package:cw_core/unspent_transaction_output.dart'; -import 'package:cw_wownero/api/structs/coins_info_row.dart'; class WowneroUnspent extends Unspent { WowneroUnspent( @@ -8,13 +7,5 @@ class WowneroUnspent extends Unspent { this.isFrozen = isFrozen; } - factory WowneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) => WowneroUnspent( - coinsInfoRow.getAddress(), - coinsInfoRow.getHash(), - coinsInfoRow.getKeyImage(), - coinsInfoRow.amount, - coinsInfoRow.frozen == 1, - coinsInfoRow.unlocked == 1); - final bool isUnlocked; } diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 52f84e26a..e02c0ec2e 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -107,9 +107,7 @@ abstract class WowneroWalletBase @override String get seed => wownero_wallet.getSeed(); - String seedLegacy(String? language) { - return wownero_wallet.getSeedLegacy(language); - } + String seedLegacy(String? language) => wownero_wallet.getSeedLegacy(language); @override MoneroWalletKeys get keys => MoneroWalletKeys( @@ -182,12 +180,12 @@ abstract class WowneroWalletBase @override Future startSync() async { try { - _setInitialHeight(); + _assertInitialHeight(); } catch (_) { // our restore height wasn't correct, so lets see if using the backup works: try { await resetCache(name); - _setInitialHeight(); + _assertInitialHeight(); } catch (e) { // we still couldn't get a valid height from the backup?!: // try to use the date instead: @@ -604,18 +602,14 @@ abstract class WowneroWalletBase _listener = wownero_wallet.setListeners(_onNewBlock, _onNewTransaction); } - // check if the height is correct: - void _setInitialHeight() { - if (walletInfo.isRecovery) { - return; - } + /// Asserts the current height to be above [MIN_RESTORE_HEIGHT] + void _assertInitialHeight() { + if (walletInfo.isRecovery) return; final height = wownero_wallet.getCurrentHeight(); - if (height > MIN_RESTORE_HEIGHT) { - // the restore height is probably correct, so we do nothing: - return; - } + // the restore height is probably correct, so we do nothing: + if (height > MIN_RESTORE_HEIGHT) return; throw Exception("height isn't > $MIN_RESTORE_HEIGHT!"); } diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 011fed169..d91922ac9 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -437,10 +437,10 @@ packages: monero: dependency: "direct main" description: - path: "." - ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 - resolved-ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 - url: "https://github.com/mrcyjanek/monero.dart" + path: "impls/monero.dart" + ref: "bcb328a4956105dc182afd0ce2e48fe263f5f20b" + resolved-ref: "bcb328a4956105dc182afd0ce2e48fe263f5f20b" + url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" mutex: diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 4537955ab..7a45eb628 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -24,8 +24,9 @@ dependencies: path: ../cw_core monero: git: - url: https://github.com/mrcyjanek/monero.dart - ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 + url: https://github.com/mrcyjanek/monero_c + ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b # monero_c hash + path: impls/monero.dart mutex: ^3.1.0 dev_dependencies: diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index c1384a3df..1f1888b44 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -346,4 +346,9 @@ class CWMonero extends Monero { Future getCurrentHeight() async { return monero_wallet_api.getCurrentHeight(); } + + @override + void monerocCheck() { + checkIfMoneroCIsFine(); + } } diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 11abdeb58..1cf3e3e0c 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -124,6 +124,36 @@ class CryptoBalanceWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ + Observer( + builder: (_) { + if (dashboardViewModel.getMoneroError != null) { + return Padding( + padding: const EdgeInsets.fromLTRB(16,0,16,16), + child: DashBoardRoundedCardWidget( + title: "Invalid monero bindings", + subTitle: dashboardViewModel.getMoneroError.toString(), + onTap: () {}, + ), + ); + } + return Container(); + }, + ), + Observer( + builder: (_) { + if (dashboardViewModel.getWowneroError != null) { + return Padding( + padding: const EdgeInsets.fromLTRB(16,0,16,16), + child: DashBoardRoundedCardWidget( + title: "Invalid wownero bindings", + subTitle: dashboardViewModel.getWowneroError.toString(), + onTap: () {}, + ) + ); + } + return Container(); + }, + ), Observer( builder: (_) => dashboardViewModel.balanceViewModel.hasAccounts ? HomeScreenAccountWidget( diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 06c565035..1baea76cd 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/entities/service_status.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/wownero/wownero.dart' as wow; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -336,6 +337,27 @@ abstract class DashboardViewModelBase with Store { wallet.type == WalletType.haven; @computed + String? get getMoneroError { + if (wallet.type != WalletType.monero) return null; + try { + monero!.monerocCheck(); + } catch (e) { + return e.toString(); + } + return null; + } + + @computed + String? get getWowneroError { + if (wallet.type != WalletType.wownero) return null; + try { + wow.wownero!.wownerocCheck(); + } catch (e) { + return e.toString(); + } + return null; + } + List get isMoneroWalletBrokenReasons { if (wallet.type != WalletType.monero) return []; final keys = monero!.getKeys(wallet); diff --git a/lib/wownero/cw_wownero.dart b/lib/wownero/cw_wownero.dart index 03bebc463..0e0b00fd4 100644 --- a/lib/wownero/cw_wownero.dart +++ b/lib/wownero/cw_wownero.dart @@ -347,4 +347,9 @@ class CWWownero extends Wownero { String getLegacySeed(Object wallet, String langName) => (wallet as WowneroWalletBase).seedLegacy(langName); + + @override + void wownerocCheck() { + checkIfMoneroCIsFine(); + } } diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index cac5d3ad2..2e53a54ea 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,7 +8,7 @@ if [[ ! -d "monero_c" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch rewrite-wip cd monero_c - git checkout c094ed5da69d2274747bf6edd7ca24124487bd34 + git checkout bcb328a4956105dc182afd0ce2e48fe263f5f20b git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero diff --git a/tool/configure.dart b/tool/configure.dart index 853d06448..32b470979 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -262,6 +262,7 @@ import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_monero/monero_unspent.dart'; import 'package:cw_monero/monero_wallet_service.dart'; +import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:cw_monero/monero_transaction_info.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; @@ -377,6 +378,7 @@ abstract class Monero { double formatterMoneroAmountToDouble({required int amount}); int formatterMoneroParseAmount({required String amount}); Account getCurrentAccount(Object wallet); + void monerocCheck(); void setCurrentAccount(Object wallet, int id, String label, String? balance); void onStartup(); int getTransactionInfoAccountId(TransactionInfo tx); @@ -449,6 +451,7 @@ import 'package:cw_wownero/wownero_transaction_info.dart'; import 'package:cw_wownero/wownero_transaction_creation_credentials.dart'; import 'package:cw_core/account.dart' as wownero_account; import 'package:cw_wownero/api/wallet.dart' as wownero_wallet_api; +import 'package:cw_wownero/api/wallet_manager.dart'; import 'package:cw_wownero/mnemonics/english.dart'; import 'package:cw_wownero/mnemonics/chinese_simplified.dart'; import 'package:cw_wownero/mnemonics/dutch.dart'; @@ -540,6 +543,7 @@ abstract class Wownero { Future updateUnspents(Object wallet); Future getCurrentHeight(); + void wownerocCheck(); WalletCredentials createWowneroRestoreWalletFromKeysCredentials({ required String name, From 8e4082d6806a89a256f827952036952a9ed21b47 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 9 Aug 2024 22:18:32 +0300 Subject: [PATCH 011/142] Generic fixes (#1583) * add litecoin nodes minor ui fix * update build macos to build universal archs [skip ci] * minor fix [skip ci] * update share package * change trocador onion url --- lib/anonpay/anonpay_api.dart | 2 +- .../screens/new_wallet/new_wallet_page.dart | 30 ++++++++++--------- .../address_edit_or_create_page.dart | 6 ++-- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- pubspec_base.yaml | 2 +- scripts/macos/build_all.sh | 2 +- .../flutter/generated_plugin_registrant.cc | 3 ++ windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/anonpay/anonpay_api.dart b/lib/anonpay/anonpay_api.dart index e46499407..acab662d1 100644 --- a/lib/anonpay/anonpay_api.dart +++ b/lib/anonpay/anonpay_api.dart @@ -20,7 +20,7 @@ class AnonPayApi { final WalletBase wallet; static const anonpayRef = secrets.anonPayReferralCode; - static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; + static const onionApiAuthority = 'tqzngtf2hybjbexznel6dhgsvbynjzezoybvtv6iofomx7gchqfssgqd.onion'; static const clearNetAuthority = 'trocador.app'; static const markup = secrets.trocadorExchangeMarkup; static const anonPayPath = '/anonpay'; diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 306c41479..d9427af0a 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -40,11 +40,11 @@ class NewWalletPage extends BasePage { @override Function(BuildContext)? get pushToNextWidget => (context) { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.focusedChild?.unfocus(); - } - }; + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; @override Widget body(BuildContext context) => WalletNameForm( @@ -88,15 +88,17 @@ class _WalletNameFormState extends State { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( - context: context, - builder: (_) { - return AlertWithOneAction( - alertTitle: S.current.new_wallet, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); + if (context.mounted) { + showPopUp( + context: context, + builder: (_) { + return AlertWithOneAction( + alertTitle: S.current.new_wallet, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } }); } }); diff --git a/lib/src/screens/subaddress/address_edit_or_create_page.dart b/lib/src/screens/subaddress/address_edit_or_create_page.dart index e067c78d0..b69a6d8df 100644 --- a/lib/src/screens/subaddress/address_edit_or_create_page.dart +++ b/lib/src/screens/subaddress/address_edit_or_create_page.dart @@ -58,7 +58,7 @@ class AddressEditOrCreatePage extends BasePage { isLoading: addressEditOrCreateViewModel.state is AddressIsSaving, isDisabled: - addressEditOrCreateViewModel.label?.isEmpty ?? true, + addressEditOrCreateViewModel.label.isEmpty, ), ) ], @@ -74,7 +74,9 @@ class AddressEditOrCreatePage extends BasePage { (AddressEditOrCreateState state) { if (state is AddressSavedSuccessfully) { WidgetsBinding.instance - .addPostFrameCallback((_) => Navigator.of(context).pop()); + .addPostFrameCallback((_) { + if (context.mounted) Navigator.of(context).pop(); + }); } }); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 873d50649..338ece4ce 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,7 +15,7 @@ import in_app_review import package_info import package_info_plus import path_provider_foundation -import share_plus_macos +import share_plus import shared_preferences_foundation import url_launcher_macos import wakelock_plus diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 84b4631fc..463c04988 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -21,7 +21,7 @@ dependencies: mobx: ^2.1.4 flutter_mobx: ^2.0.6+5 flutter_slidable: ^3.0.1 - share_plus: ^4.0.10 + share_plus: ^10.0.0 # date_range_picker: ^1.0.6 #https://api.flutter.dev/flutter/material/showDateRangePicker.html dio: ^4.0.6 diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh index 4116704bf..030617f7d 100755 --- a/scripts/macos/build_all.sh +++ b/scripts/macos/build_all.sh @@ -1,3 +1,3 @@ #!/bin/sh -./build_monero_all.sh \ No newline at end of file +./build_monero_all.sh universal \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 323f53c9f..c6444e09c 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d6d9b0a49..0a0b2f9eb 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_local_authentication flutter_secure_storage_windows permission_handler_windows + share_plus url_launcher_windows ) From fb33a6f23dce32172d9b80f343972dc4e07289c3 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 9 Aug 2024 23:15:30 +0300 Subject: [PATCH 012/142] Cw 688 avoid wallet file corruption (#1582) * CW-688 Store Seed and keys in .keys file * CW-688 Open wallet from keys in .keys file and migrate wallets using the old file * CW-688 Open wallet from keys in .keys file and migrate wallets using the old file * CW-688 Restore .keys file from .keys.backup * CW-688 Restore .keys file from .keys.backup * CW-688 Move saving .keys files into the save function instead of the service * CW-688 Handle corrupt wallets * CW-688 Handle corrupt wallets * CW-688 Remove code duplication * CW-688 Reduce cache dependency * wrap any file reading/writing function with try/catch [skip ci] --------- Co-authored-by: Konstantin Ullrich --- cw_bitcoin/lib/bitcoin_wallet.dart | 66 +++++++---- cw_bitcoin/lib/bitcoin_wallet_service.dart | 2 + cw_bitcoin/lib/electrum_wallet.dart | 14 ++- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 8 +- cw_bitcoin/lib/litecoin_wallet.dart | 55 ++++++--- cw_bitcoin/lib/litecoin_wallet_service.dart | 1 + .../lib/src/bitcoin_cash_wallet.dart | 35 ++++-- .../lib/src/bitcoin_cash_wallet_service.dart | 2 + cw_core/lib/wallet_keys_file.dart | 110 ++++++++++++++++++ cw_ethereum/lib/ethereum_wallet.dart | 33 ++++-- cw_evm/lib/evm_chain_wallet.dart | 11 +- cw_nano/lib/nano_wallet.dart | 81 ++++++++----- cw_nano/lib/nano_wallet_service.dart | 2 +- cw_polygon/lib/polygon_wallet.dart | 37 ++++-- cw_polygon/lib/polygon_wallet_service.dart | 2 - cw_solana/lib/solana_wallet.dart | 47 ++++++-- cw_tron/lib/tron_wallet.dart | 66 +++++++---- cw_tron/lib/tron_wallet_service.dart | 5 +- 18 files changed, 433 insertions(+), 144 deletions(-) create mode 100644 cw_core/lib/wallet_keys_file.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index d061480ed..ce3e2caa8 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -6,15 +6,16 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:convert/convert.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/electrum_derivations.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/psbt_transaction_builder.dart'; import 'package:cw_core/crypto_currency.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'; @@ -143,49 +144,66 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final network = walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) : BitcoinNetwork.mainnet; - final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network); - walletInfo.derivationInfo ??= DerivationInfo( - derivationType: snp.derivationType ?? DerivationType.electrum, - derivationPath: snp.derivationPath, - ); + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); + + ElectrumWalletSnapshot? snp = null; + + try { + snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network); + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + keysData = + WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + + walletInfo.derivationInfo ??= DerivationInfo(); // set the default if not present: - walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? electrum_path; - walletInfo.derivationInfo!.derivationType = snp.derivationType ?? DerivationType.electrum; + walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; + final mnemonic = keysData.mnemonic; + final passphrase = keysData.passphrase; - if (snp.mnemonic != null) { + if (mnemonic != null) { switch (walletInfo.derivationInfo!.derivationType) { case DerivationType.electrum: - seedBytes = await mnemonicToSeedBytes(snp.mnemonic!); + seedBytes = await mnemonicToSeedBytes(mnemonic); break; case DerivationType.bip39: default: seedBytes = await bip39.mnemonicToSeed( - snp.mnemonic!, - passphrase: snp.passphrase ?? '', + mnemonic, + passphrase: passphrase ?? '', ); break; } } return BitcoinWallet( - mnemonic: snp.mnemonic, - xpub: snp.xpub, + mnemonic: mnemonic, + xpub: keysData.xPub, password: password, - passphrase: snp.passphrase, + passphrase: passphrase, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses, - initialSilentAddresses: snp.silentAddresses, - initialSilentAddressIndex: snp.silentAddressIndex, - initialBalance: snp.balance, + initialAddresses: snp?.addresses, + initialSilentAddresses: snp?.silentAddresses, + initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, + initialBalance: snp?.balance, seedBytes: seedBytes, - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex, - addressPageType: snp.addressPageType, + initialRegularAddressIndex: snp?.regularAddressIndex, + initialChangeAddressIndex: snp?.changeAddressIndex, + addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: alwaysScan, ); @@ -249,8 +267,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final accountPath = walletInfo.derivationInfo?.derivationPath; final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; - final signature = await _bitcoinLedgerApp! - .signMessage(_ledgerDevice!, message: ascii.encode(message), signDerivationPath: derivationPath); + final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!, + message: ascii.encode(message), signDerivationPath: derivationPath); return base64Encode(signature); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index a9a6d96db..cf93aa29d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -41,8 +41,10 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, network: network, ); + await wallet.save(); await wallet.init(); + return wallet; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 39cf95009..e55e5ed0e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -37,6 +37,7 @@ import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:flutter/foundation.dart'; @@ -54,7 +55,7 @@ const int TWEAKS_COUNT = 25; abstract class ElectrumWalletBase extends WalletBase - with Store { + with Store, WalletKeysFile { ElectrumWalletBase({ required String password, required WalletInfo walletInfo, @@ -169,6 +170,10 @@ abstract class ElectrumWalletBase @override String? get seed => _mnemonic; + @override + WalletKeysData get walletKeysData => + WalletKeysData(mnemonic: _mnemonic, xPub: xpub, passphrase: passphrase); + bitcoin.NetworkType networkType; BasedUtxoNetwork network; @@ -1076,6 +1081,11 @@ abstract class ElectrumWalletBase @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + final path = await makePath(); await write(path: path, password: _password, data: toJSON()); await transactionHistory.save(); @@ -1131,8 +1141,6 @@ abstract class ElectrumWalletBase _autoSaveTimer?.cancel(); } - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - @action Future updateAllUnspents() async { List updatedUnspentCoins = []; diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 15ad1cf63..082460f72 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -32,15 +32,21 @@ class ElectrumWalletSnapshot { final WalletType type; final String? addressPageType; + @deprecated String? mnemonic; + + @deprecated String? xpub; + + @deprecated + String? passphrase; + List addresses; List silentAddresses; ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; int silentAddressIndex; - String? passphrase; DerivationType? derivationType; String? derivationPath; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 209ddc774..bfb9a1b16 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,20 +1,21 @@ +import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_priority.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:mobx/mobx.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; -import 'package:cw_bitcoin/electrum_wallet.dart'; -import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; -import 'package:cw_bitcoin/litecoin_network.dart'; -import 'package:bip39/bip39.dart' as bip39; part 'litecoin_wallet.g.dart'; @@ -101,19 +102,37 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = - await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet); + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); + + ElectrumWalletSnapshot? snp = null; + + try { + snp = await ElectrumWalletSnapshot.load( + name, walletInfo.type, password, LitecoinNetwork.mainnet); + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + keysData = + WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + return LitecoinWallet( - mnemonic: snp.mnemonic!, + mnemonic: keysData.mnemonic!, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses, - initialBalance: snp.balance, - seedBytes: await mnemonicToSeedBytes(snp.mnemonic!), - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex, - addressPageType: snp.addressPageType, + initialAddresses: snp?.addresses, + initialBalance: snp?.balance, + seedBytes: await mnemonicToSeedBytes(keysData.mnemonic!), + initialRegularAddressIndex: snp?.regularAddressIndex, + initialChangeAddressIndex: snp?.changeAddressIndex, + addressPageType: snp?.addressPageType, ); } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index bb51a4eaa..7025b72e5 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -33,6 +33,7 @@ class LitecoinWalletService extends WalletService< passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 51bd3612d..f15eed10d 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -12,6 +12,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_priority.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:mobx/mobx.dart'; @@ -89,14 +90,32 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWalletSnapshot.load( - name, walletInfo.type, password, BitcoinCashNetwork.mainnet); + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); + + ElectrumWalletSnapshot? snp = null; + + try { + snp = await ElectrumWalletSnapshot.load( + name, walletInfo.type, password, BitcoinCashNetwork.mainnet); + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + keysData = + WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + return BitcoinCashWallet( - mnemonic: snp.mnemonic!, + mnemonic: keysData.mnemonic!, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses.map((addr) { + initialAddresses: snp?.addresses.map((addr) { try { BitcoinCashAddress(addr.address); return BitcoinAddressRecord( @@ -116,10 +135,10 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { ); } }).toList(), - initialBalance: snp.balance, - seedBytes: await Mnemonic.toSeed(snp.mnemonic!), - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex, + initialBalance: snp?.balance, + seedBytes: await Mnemonic.toSeed(keysData.mnemonic!), + initialRegularAddressIndex: snp?.regularAddressIndex, + initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, ); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index e6c0cad07..01ae8ace3 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -34,8 +34,10 @@ class BitcoinCashWalletService extends WalletService + on WalletBase { + Future makePath() => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + // this needs to be overridden + WalletKeysData get walletKeysData; + + Future makeKeysFilePath() async => "${await makePath()}.keys"; + + Future saveKeysFile(String password, [bool isBackup = false]) async { + try { + final rootPath = await makeKeysFilePath(); + final path = "$rootPath${isBackup ? ".backup" : ""}"; + dev.log("Saving .keys file '$path'"); + await write(path: path, password: password, data: walletKeysData.toJSON()); + } catch (_) {} + } + + static Future createKeysFile( + String name, WalletType type, String password, WalletKeysData walletKeysData, + [bool withBackup = true]) async { + try { + final rootPath = await pathForWallet(name: name, type: type); + final path = "$rootPath.keys"; + + dev.log("Saving .keys file '$path'"); + await write(path: path, password: password, data: walletKeysData.toJSON()); + + if (withBackup) { + dev.log("Saving .keys.backup file '$path.backup'"); + await write(path: "$path.backup", password: password, data: walletKeysData.toJSON()); + } + } catch (_) {} + } + + static Future hasKeysFile(String name, WalletType type) async { + try { + final path = await pathForWallet(name: name, type: type); + return File("$path.keys").existsSync() || File("$path.keys.backup").existsSync(); + } catch (_) { + return false; + } + } + + static Future readKeysFile(String name, WalletType type, String password) async { + final path = await pathForWallet(name: name, type: type); + + var readPath = "$path.keys"; + try { + if (!File(readPath).existsSync()) throw Exception("No .keys file found for $name $type"); + + final jsonSource = await read(path: readPath, password: password); + final data = json.decode(jsonSource) as Map; + return WalletKeysData.fromJSON(data); + } catch (e) { + dev.log("Failed to read .keys file. Trying .keys.backup file..."); + + readPath = "$readPath.backup"; + if (!File(readPath).existsSync()) + throw Exception("No .keys nor a .keys.backup file found for $name $type"); + + final jsonSource = await read(path: readPath, password: password); + final data = json.decode(jsonSource) as Map; + final keysData = WalletKeysData.fromJSON(data); + + dev.log("Restoring .keys from .keys.backup"); + createKeysFile(name, type, password, keysData, false); + return keysData; + } + } +} + +class WalletKeysData { + final String? privateKey; + final String? mnemonic; + final String? altMnemonic; + final String? passphrase; + final String? xPub; + + WalletKeysData({this.privateKey, this.mnemonic, this.altMnemonic, this.passphrase, this.xPub}); + + String toJSON() => jsonEncode({ + "privateKey": privateKey, + "mnemonic": mnemonic, + if (altMnemonic != null) "altMnemonic": altMnemonic, + if (passphrase != null) "passphrase": passphrase, + if (xPub != null) "xPub": xPub + }); + + static WalletKeysData fromJSON(Map json) => WalletKeysData( + privateKey: json["privateKey"] as String?, + mnemonic: json["mnemonic"] as String?, + altMnemonic: json["altMnemonic"] as String?, + passphrase: json["passphrase"] as String?, + xPub: json["xPub"] as String?, + ); +} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index 2c58cd31d..7bcd55cf4 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -6,6 +6,7 @@ import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart'; import 'package:cw_ethereum/ethereum_client.dart'; import 'package:cw_ethereum/ethereum_transaction_history.dart'; @@ -122,19 +123,37 @@ class EthereumWallet extends EVMChainWallet { static Future open( {required String name, required String password, required WalletInfo walletInfo}) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String?; - final privateKey = data['private_key'] as String?; - final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ?? + + Map? data; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ?? EVMChainERC20Balance(BigInt.zero); + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + return EthereumWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, - privateKey: privateKey, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, initialBalance: balance, client: EthereumClient(), ); diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 760c50a04..55dcea959 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -16,6 +16,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_evm/evm_chain_client.dart'; import 'package:cw_evm/evm_chain_exceptions.dart'; @@ -58,7 +59,7 @@ abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet; abstract class EVMChainWalletBase extends WalletBase - with Store { + with Store, WalletKeysFile { EVMChainWalletBase({ required WalletInfo walletInfo, required EVMChainClient client, @@ -508,6 +509,11 @@ abstract class EVMChainWalletBase @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + await walletAddresses.updateAddressesInBox(); final path = await makePath(); await write(path: path, password: _password, data: toJSON()); @@ -522,7 +528,8 @@ abstract class EVMChainWalletBase ? HEX.encode((evmChainPrivateKey as EthPrivateKey).privateKey) : null; - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + @override + WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); String toJSON() => json.encode({ 'mnemonic': _mnemonic, diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 5efe3006d..55e01d10b 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -1,8 +1,12 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:bip39/bip39.dart' as bip39; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/n2_node.dart'; +import 'package:cw_core/nano_account.dart'; import 'package:cw_core/nano_account_info_response.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -10,23 +14,20 @@ import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_nano/file.dart'; -import 'package:cw_core/nano_account.dart'; -import 'package:cw_core/n2_node.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_client.dart'; import 'package:cw_nano/nano_transaction_credentials.dart'; import 'package:cw_nano/nano_transaction_history.dart'; import 'package:cw_nano/nano_transaction_info.dart'; +import 'package:cw_nano/nano_wallet_addresses.dart'; import 'package:cw_nano/nano_wallet_keys.dart'; import 'package:cw_nano/pending_nano_transaction.dart'; import 'package:mobx/mobx.dart'; -import 'dart:async'; -import 'package:cw_nano/nano_wallet_addresses.dart'; -import 'package:cw_core/wallet_base.dart'; import 'package:nanodart/nanodart.dart'; -import 'package:bip39/bip39.dart' as bip39; import 'package:nanoutil/nanoutil.dart'; part 'nano_wallet.g.dart'; @@ -34,7 +35,8 @@ part 'nano_wallet.g.dart'; class NanoWallet = NanoWalletBase with _$NanoWallet; abstract class NanoWalletBase - extends WalletBase with Store { + extends WalletBase + with Store, WalletKeysFile { NanoWalletBase({ required WalletInfo walletInfo, required String mnemonic, @@ -70,6 +72,7 @@ abstract class NanoWalletBase String? _representativeAddress; int repScore = 100; + bool get isRepOk => repScore >= 90; late final NanoClient _client; @@ -128,14 +131,10 @@ abstract class NanoWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) { - return 0; // always 0 :) - } + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; // always 0 :) @override - Future changePassword(String password) { - throw UnimplementedError("changePassword"); - } + Future changePassword(String password) => throw UnimplementedError("changePassword"); @override void close() { @@ -170,9 +169,7 @@ abstract class NanoWalletBase } @override - Future connectToPowNode({required Node node}) async { - _client.connectPow(node); - } + Future connectToPowNode({required Node node}) async => _client.connectPow(node); @override Future createTransaction(Object credentials) async { @@ -296,9 +293,7 @@ abstract class NanoWalletBase } @override - NanoWalletKeys get keys { - return NanoWalletKeys(seedKey: _hexSeed!); - } + NanoWalletKeys get keys => NanoWalletKeys(seedKey: _hexSeed!); @override String? get privateKey => _privateKey!; @@ -312,6 +307,11 @@ abstract class NanoWalletBase @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + await walletAddresses.updateAddressesInBox(); final path = await makePath(); await write(path: path, password: _password, data: toJSON()); @@ -323,6 +323,9 @@ abstract class NanoWalletBase String get hexSeed => _hexSeed!; + @override + WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, altMnemonic: hexSeed); + String get representative => _representativeAddress ?? ""; @action @@ -358,8 +361,6 @@ abstract class NanoWalletBase } } - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - String toJSON() => json.encode({ 'seedKey': _hexSeed, 'mnemonic': _mnemonic, @@ -373,31 +374,47 @@ abstract class NanoWalletBase required String password, required WalletInfo walletInfo, }) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String; + Map? data = null; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } final balance = NanoBalance.fromRawString( - currentBalance: data['currentBalance'] as String? ?? "0", - receivableBalance: data['receivableBalance'] as String? ?? "0", + currentBalance: data?['currentBalance'] as String? ?? "0", + receivableBalance: data?['receivableBalance'] as String? ?? "0", ); + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String; + final isHexSeed = !mnemonic.contains(' '); + + keysData = WalletKeysData( + mnemonic: isHexSeed ? null : mnemonic, altMnemonic: isHexSeed ? mnemonic : null); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + DerivationType derivationType = DerivationType.nano; - if (data['derivationType'] == "DerivationType.bip39") { + if (data?['derivationType'] == "DerivationType.bip39") { derivationType = DerivationType.bip39; } walletInfo.derivationInfo ??= DerivationInfo(derivationType: derivationType); - if (walletInfo.derivationInfo!.derivationType == null) { - walletInfo.derivationInfo!.derivationType = derivationType; - } + walletInfo.derivationInfo!.derivationType ??= derivationType; return NanoWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, + mnemonic: keysData.mnemonic!, initialBalance: balance, ); // init() should always be run after this! @@ -435,7 +452,7 @@ abstract class NanoWalletBase _representativeAddress = await _client.getRepFromPrefs(); throw Exception("Failed to get representative address $e"); } - + repScore = await _client.getRepScore(_representativeAddress!); } diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index a1af3c872..755598705 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -39,7 +39,7 @@ class NanoWalletService extends WalletService open( {required String name, required String password, required WalletInfo walletInfo}) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String?; - final privateKey = data['private_key'] as String?; - final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ?? + + Map? data; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ?? EVMChainERC20Balance(BigInt.zero); + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + return PolygonWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, - privateKey: privateKey, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, initialBalance: balance, client: PolygonClient(), ); diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index ee84a014e..14baffc44 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -35,7 +35,6 @@ class PolygonWalletService extends EVMChainWalletService { await wallet.init(); wallet.addInitialTokens(); await wallet.save(); - return wallet; } @@ -83,7 +82,6 @@ class PolygonWalletService extends EVMChainWalletService { await wallet.init(); wallet.addInitialTokens(); await wallet.save(); - return wallet; } diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 401968698..2b30a204c 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; + import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; @@ -12,6 +13,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_solana/default_spl_tokens.dart'; import 'package:cw_solana/file.dart'; import 'package:cw_solana/solana_balance.dart'; @@ -36,7 +38,8 @@ part 'solana_wallet.g.dart'; class SolanaWallet = SolanaWalletBase with _$SolanaWallet; abstract class SolanaWalletBase - extends WalletBase with Store { + extends WalletBase + with Store, WalletKeysFile { SolanaWalletBase({ required WalletInfo walletInfo, String? mnemonic, @@ -121,6 +124,9 @@ abstract class SolanaWalletBase return privateKey; } + @override + WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); + Future init() async { final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}"; @@ -336,6 +342,11 @@ abstract class SolanaWalletBase @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + await walletAddresses.updateAddressesInBox(); final path = await makePath(); await write(path: path, password: _password, data: toJSON()); @@ -361,8 +372,6 @@ abstract class SolanaWalletBase } } - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - String toJSON() => json.encode({ 'mnemonic': _mnemonic, 'private_key': _hexPrivateKey, @@ -374,18 +383,36 @@ abstract class SolanaWalletBase required String password, required WalletInfo walletInfo, }) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String?; - final privateKey = data['private_key'] as String?; - final balance = SolanaBalance.fromJSON(data['balance'] as String) ?? SolanaBalance(0.0); + + Map? data; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = SolanaBalance.fromJSON(data?['balance'] as String) ?? SolanaBalance(0.0); + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } return SolanaWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, - privateKey: privateKey, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, initialBalance: balance, ); } diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index 96f92e450..cb4c9c024 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -16,6 +16,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_tron/default_tron_tokens.dart'; import 'package:cw_tron/file.dart'; @@ -37,7 +38,8 @@ part 'tron_wallet.g.dart'; class TronWallet = TronWalletBase with _$TronWallet; abstract class TronWalletBase - extends WalletBase with Store { + extends WalletBase + with Store, WalletKeysFile { TronWalletBase({ required WalletInfo walletInfo, String? mnemonic, @@ -124,18 +126,36 @@ abstract class TronWalletBase required String password, required WalletInfo walletInfo, }) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String?; - final privateKey = data['private_key'] as String?; - final balance = TronBalance.fromJSON(data['balance'] as String) ?? TronBalance(BigInt.zero); + + Map? data; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = TronBalance.fromJSON(data?['balance'] as String) ?? TronBalance(BigInt.zero); + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } return TronWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, - privateKey: privateKey, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, initialBalance: balance, ); } @@ -163,9 +183,7 @@ abstract class TronWalletBase }) async { assert(mnemonic != null || privateKey != null); - if (privateKey != null) { - return TronPrivateKey(privateKey); - } + if (privateKey != null) return TronPrivateKey(privateKey); final seed = bip39.mnemonicToSeed(mnemonic!); @@ -181,14 +199,10 @@ abstract class TronWalletBase int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; @override - Future changePassword(String password) { - throw UnimplementedError("changePassword"); - } + Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() { - _transactionsUpdateTimer?.cancel(); - } + void close() => _transactionsUpdateTimer?.cancel(); @action @override @@ -406,12 +420,15 @@ abstract class TronWalletBase Object get keys => throw UnimplementedError("keys"); @override - Future rescan({required int height}) { - throw UnimplementedError("rescan"); - } + Future rescan({required int height}) => throw UnimplementedError("rescan"); @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + await walletAddresses.updateAddressesInBox(); final path = await makePath(); await write(path: path, password: _password, data: toJSON()); @@ -424,7 +441,8 @@ abstract class TronWalletBase @override String get privateKey => _tronPrivateKey.toHex(); - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + @override + WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); String toJSON() => json.encode({ 'mnemonic': _mnemonic, @@ -512,7 +530,7 @@ abstract class TronWalletBase @override Future renameWalletFiles(String newWalletName) async { - String transactionHistoryFileNameForWallet = 'tron_transactions.json'; + const transactionHistoryFileNameForWallet = 'tron_transactions.json'; final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); final currentWalletFile = File(currentWalletPath); @@ -550,9 +568,7 @@ abstract class TronWalletBase Future signMessage(String message, {String? address}) async => _tronPrivateKey.signPersonalMessage(ascii.encode(message)); - String getTronBase58AddressFromHex(String hexAddress) { - return TronAddress(hexAddress).toAddress(); - } + String getTronBase58AddressFromHex(String hexAddress) => TronAddress(hexAddress).toAddress(); void updateScanProviderUsageState(bool isEnabled) { if (isEnabled) { diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart index c8344d5f4..ba217a265 100644 --- a/cw_tron/lib/tron_wallet_service.dart +++ b/cw_tron/lib/tron_wallet_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:bip39/bip39.dart' as bip39; +import 'package:collection/collection.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_history.dart'; @@ -14,7 +15,6 @@ import 'package:cw_tron/tron_exception.dart'; import 'package:cw_tron/tron_wallet.dart'; import 'package:cw_tron/tron_wallet_creation_credentials.dart'; import 'package:hive/hive.dart'; -import 'package:collection/collection.dart'; class TronWalletService extends WalletService< TronNewWalletCredentials, @@ -153,7 +153,8 @@ class TronWalletService extends WalletService< } @override - Future, TransactionInfo>> restoreFromHardwareWallet(TronNewWalletCredentials credentials) { + Future, TransactionInfo>> + restoreFromHardwareWallet(TronNewWalletCredentials credentials) { // TODO: implement restoreFromHardwareWallet throw UnimplementedError(); } From 14e99daa7398791cff5dbb9134e66f86f2142043 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 10 Aug 2024 00:48:36 +0300 Subject: [PATCH 013/142] align the hint with the prefix in the text field (#1571) * Update send_card.dart * update currency amount text field widget * Update qr_widget.dart --- lib/src/screens/exchange/exchange_page.dart | 39 +-- .../exchange/widgets/exchange_card.dart | 131 +------- .../receive/widgets/currency_input_field.dart | 301 +++++++++++------- .../screens/receive/widgets/qr_widget.dart | 44 ++- lib/src/screens/send/send_template_page.dart | 2 +- .../widgets/prefix_currency_icon_widget.dart | 65 ---- lib/src/screens/send/widgets/send_card.dart | 228 ++----------- .../send/widgets/send_template_card.dart | 183 +++++------ .../send/send_template_view_model.dart | 5 + lib/view_model/send/template_view_model.dart | 23 +- 10 files changed, 371 insertions(+), 650 deletions(-) delete mode 100644 lib/src/screens/send/widgets/prefix_currency_icon_widget.dart diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 5c064df27..2c717a3c8 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -67,17 +67,6 @@ class ExchangePage extends BasePage { Debounce _depositAmountDebounce = Debounce(Duration(milliseconds: 500)); var _isReactionsSet = false; - final arrowBottomPurple = Image.asset( - 'assets/images/arrow_bottom_purple_icon.png', - color: Colors.white, - height: 8, - ); - final arrowBottomCakeGreen = Image.asset( - 'assets/images/arrow_bottom_cake_green.png', - color: Colors.white, - height: 8, - ); - late final String? depositWalletName; late final String? receiveWalletName; @@ -101,11 +90,11 @@ class ExchangePage extends BasePage { @override Function(BuildContext)? get pushToNextWidget => (context) { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.focusedChild?.unfocus(); - } - }; + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; @override Widget middle(BuildContext context) => Row( @@ -340,7 +329,6 @@ class ExchangePage extends BasePage { void applyTemplate( BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async { - final depositCryptoCurrency = CryptoCurrency.fromString(template.depositCurrency); final receiveCryptoCurrency = CryptoCurrency.fromString(template.receiveCurrency); @@ -354,10 +342,12 @@ class ExchangePage extends BasePage { exchangeViewModel.isFixedRateMode = false; var domain = template.depositAddress; - exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, depositCryptoCurrency); + exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, depositCryptoCurrency); domain = template.receiveAddress; - exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, receiveCryptoCurrency); + exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, receiveCryptoCurrency); } void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) { @@ -529,14 +519,16 @@ class ExchangePage extends BasePage { _depositAddressFocus.addListener(() async { if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) { final domain = depositAddressController.text; - exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency); + exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency); } }); _receiveAddressFocus.addListener(() async { if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) { final domain = receiveAddressController.text; - exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency); + exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency); } }); @@ -589,7 +581,8 @@ class ExchangePage extends BasePage { } } - Future fetchParsedAddress(BuildContext context, String domain, CryptoCurrency currency) async { + Future fetchParsedAddress( + BuildContext context, String domain, CryptoCurrency currency) async { final parsedAddress = await getIt.get().resolve(context, domain, currency); final address = await extractAddressFromParsed(context, parsedAddress); return address; @@ -658,7 +651,6 @@ class ExchangePage extends BasePage { exchangeViewModel.changeDepositCurrency(currency: currency); }, - imageArrow: arrowBottomPurple, currencyButtonColor: Colors.transparent, addressButtonsColor: Theme.of(context).extension()!.textFieldButtonColor, @@ -705,7 +697,6 @@ class ExchangePage extends BasePage { currencies: exchangeViewModel.receiveCurrencies, onCurrencySelected: (currency) => exchangeViewModel.changeReceiveCurrency(currency: currency), - imageArrow: arrowBottomCakeGreen, currencyButtonColor: Colors.transparent, addressButtonsColor: Theme.of(context).extension()!.textFieldButtonColor, diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 760b0c137..02218f848 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/entities/contact_base.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; import 'package:cake_wallet/themes/extensions/qr_code_theme.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; @@ -27,7 +28,7 @@ class ExchangeCard extends StatefulWidget { required this.isAmountEstimated, required this.currencies, required this.onCurrencySelected, - required this.imageArrow, + this.imageArrow, this.currencyValueValidator, this.addressTextFieldValidator, this.title = '', @@ -58,7 +59,7 @@ class ExchangeCard extends StatefulWidget { final bool isAmountEstimated; final bool hasRefundAddress; final bool isMoneroWallet; - final Image imageArrow; + final Image? imageArrow; final Color currencyButtonColor; final Color? addressButtonsColor; final Color borderColor; @@ -191,120 +192,18 @@ class ExchangeCardState extends State { ) ], ), - Padding( - padding: EdgeInsets.only(top: 20), - child: Row( - children: [ - Container( - padding: EdgeInsets.only(right: 8), - height: 32, - color: widget.currencyButtonColor, - child: InkWell( - onTap: () => _presentPicker(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.only(right: 5), - child: widget.imageArrow, - ), - Text(_selectedCurrency.toString(), - style: TextStyle( - fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white)) - ]), - ), - ), - if (_selectedCurrency.tag != null) - Padding( - padding: const EdgeInsets.only(right: 3.0), - child: Container( - height: 32, - decoration: BoxDecoration( - color: widget.addressButtonsColor ?? - Theme.of(context).extension()!.textFieldButtonColor, - borderRadius: BorderRadius.all(Radius.circular(6))), - child: Center( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text(_selectedCurrency.tag!, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .extension()! - .textFieldButtonIconColor)), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Text(':', - style: TextStyle( - fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white)), - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: FocusTraversalOrder( - order: NumericFocusOrder(1), - child: BaseTextFormField( - focusNode: widget.amountFocusNode, - controller: amountController, - enabled: _isAmountEditable, - textAlign: TextAlign.left, - keyboardType: - TextInputType.numberWithOptions(signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) - ], - hintText: '0.0000', - borderColor: Colors.transparent, - //widget.borderColor, - textStyle: TextStyle( - fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), - placeholderTextStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .extension()! - .hintTextColor), - validator: _isAmountEditable - ? widget.currencyValueValidator - : null), - ), - ), - if (widget.hasAllAmount) - Container( - height: 32, - width: 32, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldButtonColor, - borderRadius: BorderRadius.all(Radius.circular(6))), - child: InkWell( - onTap: () => widget.allAmount?.call(), - child: Center( - child: Text(S.of(context).all, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .extension()! - .textFieldButtonIconColor)), - ), - ), - ) - ], - ), - ), - ], - )), + CurrencyAmountTextField( + imageArrow: widget.imageArrow, + selectedCurrency: _selectedCurrency.toString(), + amountFocusNode: widget.amountFocusNode, + amountController: amountController, + onTapPicker: () => _presentPicker(context), + isAmountEditable: _isAmountEditable, + isPickerEnable: true, + allAmountButton: widget.hasAllAmount, + currencyValueValidator: widget.currencyValueValidator, + tag: _selectedCurrency.tag, + allAmountCallback: widget.allAmount), Divider(height: 1, color: Theme.of(context).extension()!.textFieldHintColor), Padding( padding: EdgeInsets.only(top: 5), diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart index 84b2a7bca..ce3de9a6c 100644 --- a/lib/src/screens/receive/widgets/currency_input_field.dart +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -1,135 +1,210 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/currency.dart'; +import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/picker_theme.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -class CurrencyInputField extends StatelessWidget { - const CurrencyInputField({ - super.key, - required this.onTapPicker, +class CurrencyAmountTextField extends StatelessWidget { + const CurrencyAmountTextField({ required this.selectedCurrency, - this.focusNode, - required this.controller, - required this.isLight, + required this.amountFocusNode, + required this.amountController, + required this.isAmountEditable, + this.allAmountButton = false, + this.isPickerEnable = false, + this.isSelected = false, + this.currentTheme = ThemeType.dark, + this.onTapPicker, + this.padding, + this.imageArrow, + this.hintText, + this.tag, + this.tagBackgroundColor, + this.currencyValueValidator, + this.allAmountCallback, }); - final Function() onTapPicker; - final Currency selectedCurrency; - final FocusNode? focusNode; - final TextEditingController controller; - final bool isLight; - - String get _currencyName { - if (selectedCurrency is CryptoCurrency) { - return (selectedCurrency as CryptoCurrency).title.toUpperCase(); - } - return selectedCurrency.name.toUpperCase(); - } + final Widget? imageArrow; + final String selectedCurrency; + final String? tag; + final String? hintText; + final Color? tagBackgroundColor; + final EdgeInsets? padding; + final FocusNode? amountFocusNode; + final TextEditingController amountController; + final bool isAmountEditable; + final FormFieldValidator? currencyValueValidator; + final bool isPickerEnable; + final ThemeType currentTheme; + final bool isSelected; + final bool allAmountButton; + final VoidCallback? allAmountCallback; + final VoidCallback? onTapPicker; @override Widget build(BuildContext context) { - final arrowBottomPurple = Image.asset( - 'assets/images/arrow_bottom_purple_icon.png', - color: Theme.of(context).extension()!.textColor, - height: 8, - ); - // This magic number for wider screen sets the text input focus at center of the inputfield - final _width = - responsiveLayoutUtil.shouldRenderMobileUI ? MediaQuery.of(context).size.width : 500; - - return Column( + final textColor = currentTheme == ThemeType.light + ? Theme.of(context).appBarTheme.titleTextStyle!.color! + : Colors.white; + final _prefixContent = Row( children: [ - Padding( - padding: EdgeInsets.only(top: 20), - child: SizedBox( - height: 40, - child: BaseTextFormField( - focusNode: focusNode, - controller: controller, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+(\.|\,)?\d{0,8}'))], - hintText: '0.000', - placeholderTextStyle: isLight - ? null - : TextStyle( - color: Theme.of(context).extension()!.textFieldBorderColor, - fontWeight: FontWeight.w600, - ), - borderColor: Theme.of(context).extension()!.dividerColor, - textColor: Theme.of(context).extension()!.textColor, - textStyle: TextStyle( - color: Theme.of(context).extension()!.textColor, - ), - prefixIcon: Padding( - padding: EdgeInsets.only( - left: _width / 4, + isPickerEnable + ? Container( + height: 32, + child: InkWell( + onTap: onTapPicker, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(right: 5), + child: imageArrow ?? + Image.asset('assets/images/arrow_bottom_purple_icon.png', + color: textColor, height: 8)), + Text( + selectedCurrency, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: textColor, + ), + ), + ], + ), ), - child: Container( - padding: EdgeInsets.only(right: 8), - child: InkWell( - onTap: onTapPicker, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.only(right: 5), - child: arrowBottomPurple, - ), - Text( - _currencyName, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - color: Theme.of(context).extension()!.textColor, - ), - ), - if (selectedCurrency.tag != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 3.0), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.textFieldButtonColor, - borderRadius: BorderRadius.all( - Radius.circular(6), - ), - ), - child: Center( - child: Text( - selectedCurrency.tag!, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).extension()!.textFieldButtonIconColor, - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 3.0), - child: Text( - ':', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 20, - color: Theme.of(context).extension()!.textColor, - ), - ), - ), - ]), + ) + : Text( + selectedCurrency, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: textColor, + ), + ), + if (tag != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3.0), + child: Container( + height: 32, + decoration: BoxDecoration( + color: tagBackgroundColor ?? + Theme.of(context).extension()!.textFieldButtonColor, + borderRadius: const BorderRadius.all(Radius.circular(6)), + ), + child: Center( + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + tag!, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).extension()!.textFieldButtonIconColor, + ), ), ), ), ), ), + Padding( + padding: EdgeInsets.only(right: 4.0), + child: Text( + ':', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: textColor, + ), + ), ), ], ); + return Padding( + padding: padding ?? const EdgeInsets.only(top: 20), + child: Row( + children: [ + isSelected + ? Container( + child: _prefixContent, + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), + margin: const EdgeInsets.only(right: 3), + decoration: BoxDecoration( + border: Border.all( + color: textColor, + ), + borderRadius: BorderRadius.circular(26), + color: Theme.of(context).primaryColor)) + : _prefixContent, + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: FocusTraversalOrder( + order: NumericFocusOrder(1), + child: BaseTextFormField( + focusNode: amountFocusNode, + controller: amountController, + enabled: isAmountEditable, + textAlign: TextAlign.left, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')), + ], + hintText: hintText ?? '0.0000', + borderColor: Colors.transparent, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: textColor, + ), + placeholderTextStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: currentTheme == ThemeType.light + ? Theme.of(context).appBarTheme.titleTextStyle!.color! + : Theme.of(context).extension()!.hintTextColor, + ), + validator: isAmountEditable ? currencyValueValidator : null, + ), + ), + ), + if (allAmountButton) + Container( + height: 32, + width: 32, + decoration: BoxDecoration( + color: Theme.of(context).extension()!.textFieldButtonColor, + borderRadius: const BorderRadius.all(Radius.circular(6)), + ), + child: InkWell( + onTap: allAmountCallback, + child: Center( + child: Text( + S.of(context).all, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .extension()! + .textFieldButtonIconColor, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); } } diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index bbfd4d5c1..9f0db059a 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -1,11 +1,15 @@ import 'package:cake_wallet/entities/qr_view_data.dart'; +import 'package:cake_wallet/themes/extensions/picker_theme.dart'; import 'package:cake_wallet/themes/extensions/qr_code_theme.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/brightness_util.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -38,6 +42,10 @@ class QRWidget extends StatelessWidget { final copyImage = Image.asset('assets/images/copy_address.png', color: Theme.of(context).extension()!.qrWidgetCopyButtonColor); + // This magic number for wider screen sets the text input focus at center of the inputfield + final _width = + responsiveLayoutUtil.shouldRenderMobileUI ? MediaQuery.of(context).size.width : 500; + return Center( child: SingleChildScrollView( child: Column( @@ -85,8 +93,9 @@ class QRWidget extends StatelessWidget { decoration: BoxDecoration( border: Border.all( width: 3, - color: - Theme.of(context).extension()!.textColor, + color: Theme.of(context) + .extension()! + .textColor, ), ), child: Container( @@ -116,20 +125,23 @@ class QRWidget extends StatelessWidget { children: [ Expanded( child: Form( - key: formKey, - child: CurrencyInputField( - focusNode: amountTextFieldFocusNode, - controller: amountController, - onTapPicker: () => _presentPicker(context), - selectedCurrency: addressListViewModel.selectedCurrency, - isLight: isLight, - ), - ), + key: formKey, + child: CurrencyAmountTextField( + selectedCurrency: _currencyName, + amountFocusNode: amountTextFieldFocusNode, + amountController: amountController, + padding: EdgeInsets.only(top: 20, left: _width / 4), + currentTheme: isLight ? ThemeType.light : ThemeType.dark, + isAmountEditable: true, + tag: addressListViewModel.selectedCurrency.tag, + onTapPicker: () => _presentPicker(context), + isPickerEnable: true)), ), ], ), ); }), + Divider(height: 1, color: Theme.of(context).extension()!.dividerColor), Padding( padding: EdgeInsets.only(top: 20, bottom: 8), child: Builder( @@ -150,7 +162,8 @@ class QRWidget extends StatelessWidget { style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor), + color: + Theme.of(context).extension()!.textColor), ), ), Padding( @@ -169,6 +182,13 @@ class QRWidget extends StatelessWidget { ); } + String get _currencyName { + if (addressListViewModel.selectedCurrency is CryptoCurrency) { + return (addressListViewModel.selectedCurrency as CryptoCurrency).title.toUpperCase(); + } + return addressListViewModel.selectedCurrency.name.toUpperCase(); + } + void _presentPicker(BuildContext context) async { await showPopUp( builder: (_) => CurrencyPicker( diff --git a/lib/src/screens/send/send_template_page.dart b/lib/src/screens/send/send_template_page.dart index 76414ecb2..f7c9da082 100644 --- a/lib/src/screens/send/send_template_page.dart +++ b/lib/src/screens/send/send_template_page.dart @@ -144,7 +144,7 @@ class SendTemplatePage extends BasePage { .toList(); sendTemplateViewModel.addTemplate( - isCurrencySelected: mainTemplate.isCurrencySelected, + isCurrencySelected: mainTemplate.isCryptoSelected, name: mainTemplate.name, address: mainTemplate.address, cryptoCurrency: mainTemplate.selectedCurrency.title, diff --git a/lib/src/screens/send/widgets/prefix_currency_icon_widget.dart b/lib/src/screens/send/widgets/prefix_currency_icon_widget.dart deleted file mode 100644 index d30349066..000000000 --- a/lib/src/screens/send/widgets/prefix_currency_icon_widget.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -import 'package:flutter/material.dart'; - -class PrefixCurrencyIcon extends StatelessWidget { - PrefixCurrencyIcon({ - required this.isSelected, - required this.title, - this.onTap, - }); - - final bool isSelected; - final String title; - final Function()? onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Padding( - padding: EdgeInsets.fromLTRB(0, 6.0, 8.0, 0), - child: Column( - children: [ - Container( - padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(26), - color: isSelected - ? Theme.of(context) - .extension()! - .templateSelectedCurrencyBackgroundColor - : Colors.transparent, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (onTap != null) - Padding( - padding: EdgeInsets.only(right: 5), - child: Image.asset( - 'assets/images/arrow_bottom_purple_icon.png', - color: Colors.white, - height: 8, - ), - ), - Text( - title + ':', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: isSelected - ? Theme.of(context) - .extension()! - .templateSelectedCurrencyTitleColor - : Colors.white, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index ec833159f..e2e7f25da 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; @@ -207,166 +208,19 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Padding( - padding: const EdgeInsets.only(top: 20), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - children: [ - sendViewModel.hasMultipleTokens - ? Container( - padding: EdgeInsets.only(right: 8), - height: 32, - child: InkWell( - onTap: () => _presentPicker(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.only(right: 5), - child: Image.asset( - 'assets/images/arrow_bottom_purple_icon.png', - color: Colors.white, - height: 8, - ), - ), - Text( - sendViewModel.selectedCryptoCurrency.title, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - color: Colors.white), - ), - ], - ), - ), - ) - : Text( - sendViewModel.selectedCryptoCurrency.title, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - color: Colors.white), - ), - sendViewModel.selectedCryptoCurrency.tag != null - ? Padding( - padding: const EdgeInsets.fromLTRB(3.0, 0, 3.0, 0), - child: Container( - height: 32, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldButtonColor, - borderRadius: BorderRadius.all( - Radius.circular(6), - )), - child: Center( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - sendViewModel.selectedCryptoCurrency.tag!, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .extension()! - .textFieldButtonIconColor), - ), - ), - ), - ), - ) - : Container(), - Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text( - ':', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - color: Colors.white), - ), - ), - ], - ), - ), - Expanded( - child: Stack( - children: [ - BaseTextFormField( - focusNode: cryptoAmountFocus, - controller: cryptoAmountController, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) - ], - suffixIcon: SizedBox( - width: prefixIconWidth, - ), - hintText: '0.0000', - borderColor: Colors.transparent, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context) - .extension()! - .textFieldHintColor, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: output.sendAll - ? sendViewModel.allAmountValidator - : sendViewModel.amountValidator, - ), - if (!sendViewModel.isBatchSending) - Positioned( - top: 2, - right: 0, - child: Container( - width: prefixIconWidth, - height: prefixIconHeight, - child: InkWell( - onTap: () async { - output.setSendAll(sendViewModel.balance); - }, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldButtonColor, - borderRadius: BorderRadius.all( - Radius.circular(6), - ), - ), - child: Center( - child: Text( - S.of(context).all, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .extension()! - .textFieldButtonIconColor, - ), - ), - ), - ), - ), - ), - ), - ], - ), - ), - ], - )), - ), + CurrencyAmountTextField( + selectedCurrency: sendViewModel.selectedCryptoCurrency.title, + amountFocusNode: cryptoAmountFocus, + amountController: cryptoAmountController, + isAmountEditable: true, + onTapPicker: () => _presentPicker(context), + isPickerEnable: sendViewModel.hasMultipleTokens, + tag: sendViewModel.selectedCryptoCurrency.tag, + allAmountButton: !sendViewModel.isBatchSending, + currencyValueValidator: output.sendAll + ? sendViewModel.allAmountValidator + : sendViewModel.amountValidator, + allAmountCallback: () async => output.setSendAll(sendViewModel.balance)), Divider( height: 1, color: Theme.of(context).extension()!.textFieldHintColor), @@ -402,41 +256,16 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin()!.textFieldBorderColor, - textStyle: TextStyle( - fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), - placeholderTextStyle: TextStyle( - color: - Theme.of(context).extension()!.textFieldHintColor, - fontWeight: FontWeight.w500, - fontSize: 14), - ), - ), + isAmountEditable: true, + allAmountButton: false), + Divider( + height: 1, + color: Theme.of(context).extension()!.textFieldHintColor), Padding( padding: EdgeInsets.only(top: 20), child: BaseTextFormField( @@ -715,12 +544,11 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin( context: context, builder: (_) => CurrencyPicker( - selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), - items: sendViewModel.currencies, - hintText: S.of(context).search_currency, - onItemSelected: (Currency cur) => - sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency), - ), + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), + items: sendViewModel.currencies, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) => + sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency)), ); } diff --git a/lib/src/screens/send/widgets/send_template_card.dart b/lib/src/screens/send/widgets/send_template_card.dart index 4f62616e3..bf2a66b73 100644 --- a/lib/src/screens/send/widgets/send_template_card.dart +++ b/lib/src/screens/send/widgets/send_template_card.dart @@ -1,5 +1,5 @@ import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; -import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -59,7 +59,8 @@ class SendTemplateCard extends StatelessWidget { hintText: sendTemplateViewModel.recipients.length > 1 ? S.of(context).template_name : S.of(context).send_name, - borderColor: Theme.of(context).extension()!.textFieldBorderColor, + borderColor: + Theme.of(context).extension()!.textFieldBorderColor, textStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), placeholderTextStyle: TextStyle( @@ -69,107 +70,87 @@ class SendTemplateCard extends StatelessWidget { validator: sendTemplateViewModel.templateValidator), Padding( padding: EdgeInsets.only(top: 20), - child: Observer( - builder: (context) { - return AddressTextField( - selectedCurrency: template.selectedCurrency, - controller: _addressController, - onURIScanned: (uri) { - final paymentRequest = PaymentRequest.fromUri(uri); - _addressController.text = paymentRequest.address; - _cryptoAmountController.text = paymentRequest.amount; - }, - options: [ - AddressTextFieldOption.paste, - AddressTextFieldOption.qrCode, - AddressTextFieldOption.addressBook - ], - onPushPasteButton: (context) async { - template.output.resetParsedAddress(); - await template.output.fetchParsedAddress(context); - }, - onPushAddressBookButton: (context) async { - template.output.resetParsedAddress(); - await template.output.fetchParsedAddress(context); - }, - buttonColor: Theme.of(context).extension()!.textFieldButtonColor, - borderColor: Theme.of(context).extension()!.textFieldBorderColor, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - hintStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textFieldHintColor, - ), - validator: sendTemplateViewModel.addressValidator, - ); - } - ), - ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Focus( - onFocusChange: (hasFocus) { - if (hasFocus) { - template.selectCurrency(); - } - }, - child: BaseTextFormField( - focusNode: _cryptoAmountFocus, - controller: _cryptoAmountController, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))], - prefixIcon: Observer( - builder: (_) => PrefixCurrencyIcon( - title: template.selectedCurrency.title, - isSelected: template.isCurrencySelected, - onTap: sendTemplateViewModel.walletCurrencies.length > 1 - ? () => _presentPicker(context) - : null, - ), - ), - hintText: '0.0000', - borderColor: Theme.of(context).extension()!.textFieldBorderColor, - textStyle: - TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context).extension()!.textFieldHintColor, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: sendTemplateViewModel.amountValidator, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Focus( - onFocusChange: (hasFocus) { - if (hasFocus) { - template.selectFiat(); - } - }, - child: BaseTextFormField( - focusNode: _fiatAmountFocus, - controller: _fiatAmountController, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))], - prefixIcon: Observer( - builder: (_) => PrefixCurrencyIcon( - title: sendTemplateViewModel.fiatCurrency, - isSelected: template.isFiatSelected)), - hintText: '0.00', - borderColor: Theme.of(context).extension()!.textFieldBorderColor, - textStyle: - TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context).extension()!.textFieldHintColor, - fontWeight: FontWeight.w500, + child: Observer(builder: (context) { + return AddressTextField( + selectedCurrency: template.selectedCurrency, + controller: _addressController, + onURIScanned: (uri) { + final paymentRequest = PaymentRequest.fromUri(uri); + _addressController.text = paymentRequest.address; + _cryptoAmountController.text = paymentRequest.amount; + }, + options: [ + AddressTextFieldOption.paste, + AddressTextFieldOption.qrCode, + AddressTextFieldOption.addressBook + ], + onPushPasteButton: (context) async { + template.output.resetParsedAddress(); + await template.output.fetchParsedAddress(context); + }, + onPushAddressBookButton: (context) async { + template.output.resetParsedAddress(); + await template.output.fetchParsedAddress(context); + }, + buttonColor: + Theme.of(context).extension()!.textFieldButtonColor, + borderColor: + Theme.of(context).extension()!.textFieldBorderColor, + textStyle: TextStyle( fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, ), - ), + hintStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textFieldHintColor, + ), + validator: sendTemplateViewModel.addressValidator, + ); + }), + ), + Focus( + onFocusChange: (hasFocus) { + if (hasFocus) template.setCryptoCurrency(true); + }, + child: Column( + children: [ + Observer( + builder: (context) => CurrencyAmountTextField( + selectedCurrency: template.selectedCurrency.title, + amountFocusNode: _cryptoAmountFocus, + amountController: _cryptoAmountController, + isSelected: template.isCryptoSelected, + tag: template.selectedCurrency.tag, + isPickerEnable: sendTemplateViewModel.hasMultipleTokens, + onTapPicker: () => _presentPicker(context), + currencyValueValidator: sendTemplateViewModel.amountValidator, + isAmountEditable: true)), + Divider( + height: 1, + color: Theme.of(context).extension()!.textFieldBorderColor) + ], + ), + ), + Focus( + onFocusChange: (hasFocus) { + if (hasFocus) template.setCryptoCurrency(false); + }, + child: Column( + children: [ + Observer( + builder: (context) => CurrencyAmountTextField( + selectedCurrency: sendTemplateViewModel.fiatCurrency, + amountFocusNode: _fiatAmountFocus, + amountController: _fiatAmountController, + isSelected: !template.isCryptoSelected, + hintText: '0.00', + isAmountEditable: true)), + Divider( + height: 1, + color: Theme.of(context).extension()!.textFieldBorderColor) + ], ), ), ], diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index 66a3c37c8..3c78f3000 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/view_model/send/template_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_type.dart'; @@ -97,4 +98,8 @@ abstract class SendTemplateViewModelBase with Store { @computed List get walletCurrencies => _wallet.balance.keys.toList(); + + bool get hasMultipleTokens => isEVMCompatibleChain(_wallet.type) || + _wallet.type == WalletType.solana || + _wallet.type == WalletType.tron; } diff --git a/lib/view_model/send/template_view_model.dart b/lib/view_model/send/template_view_model.dart index 5b799c343..fcd40bd43 100644 --- a/lib/view_model/send/template_view_model.dart +++ b/lib/view_model/send/template_view_model.dart @@ -40,35 +40,22 @@ abstract class TemplateViewModelBase with Store { CryptoCurrency _currency; @observable - bool isCurrencySelected = true; - - @observable - bool isFiatSelected = false; + bool isCryptoSelected = true; @action - void selectCurrency() { - isCurrencySelected = true; - isFiatSelected = false; - } - - @action - void selectFiat() { - isFiatSelected = true; - isCurrencySelected = false; - } + void setCryptoCurrency(bool value) => isCryptoSelected = value; @action void reset() { name = ''; address = ''; - isCurrencySelected = true; - isFiatSelected = false; + isCryptoSelected = true; output.reset(); } Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) { return Template( - isCurrencySelectedRaw: isCurrencySelected, + isCurrencySelectedRaw: isCryptoSelected, nameRaw: name, addressRaw: address, cryptoCurrencyRaw: cryptoCurrency, @@ -79,7 +66,7 @@ abstract class TemplateViewModelBase with Store { @action void changeSelectedCurrency(CryptoCurrency currency) { - isCurrencySelected = true; + isCryptoSelected = true; _currency = currency; } From acadee6ed54f58c8001b136ff4cf439894a8aed5 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 10 Aug 2024 00:49:27 +0300 Subject: [PATCH 014/142] fix custom rate issue (#1579) Co-authored-by: Omar Hatem --- lib/bitcoin/cw_bitcoin.dart | 7 +++++++ .../rbf_details_list_fee_picker_item.dart | 2 +- lib/src/widgets/standard_picker_list.dart | 5 +++-- lib/view_model/transaction_details_view_model.dart | 10 +++++++--- tool/configure.dart | 1 + 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index efb1211bc..a92aaad74 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -435,6 +435,13 @@ class CWBitcoin extends Bitcoin { ); } + @override + int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, + {int? size}) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.feeAmountWithFeeRate(feeRate, inputsCount, outputsCount, size: size); + } + @override int getMaxCustomFeeRate(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; diff --git a/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart b/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart index 7615065d7..db3d94500 100644 --- a/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart +++ b/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart @@ -17,7 +17,7 @@ class StandardPickerListItem extends TransactionDetailsListItem { final List items; final String Function(T item, double sliderValue) displayItem; final Function(double) onSliderChanged; - final Function(T) onItemSelected; + final Function(T item, double sliderValue) onItemSelected; final int selectedIdx; final double? maxValue; final int customItemIndex; diff --git a/lib/src/widgets/standard_picker_list.dart b/lib/src/widgets/standard_picker_list.dart index ea8b07097..0e9831420 100644 --- a/lib/src/widgets/standard_picker_list.dart +++ b/lib/src/widgets/standard_picker_list.dart @@ -23,7 +23,7 @@ class StandardPickerList extends StatefulWidget { final int customItemIndex; final String Function(T item, double sliderValue) displayItem; final Function(double) onSliderChanged; - final Function(T) onItemSelected; + final Function(T item, double sliderValue) onItemSelected; final String value; final int selectedIdx; final double customValue; @@ -50,6 +50,7 @@ class _StandardPickerListState extends State> { @override Widget build(BuildContext context) { String adaptedDisplayItem(T item) => widget.displayItem(item, customValue); + String adaptedOnItemSelected(T item) => widget.onItemSelected(item, customValue).toString(); return Column( children: [ @@ -74,7 +75,7 @@ class _StandardPickerListState extends State> { }, onItemSelected: (T item) { setState(() => selectedIdx = widget.items.indexOf(item)); - value = widget.onItemSelected(item).toString(); + value = adaptedOnItemSelected(item); }, ), ), diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 9e71837a7..ef6474974 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -378,9 +378,9 @@ abstract class TransactionDetailsViewModelBase with Store { sendViewModel.displayFeeRate(priority, sliderValue.round()), onSliderChanged: (double newValue) => setNewFee(value: newValue, priority: transactionPriority!), - onItemSelected: (dynamic item) { + onItemSelected: (dynamic item, double sliderValue) { transactionPriority = item as TransactionPriority; - return setNewFee(priority: transactionPriority!); + return setNewFee(value: sliderValue, priority: transactionPriority!); })); if (transactionInfo.inputAddresses != null) { @@ -427,7 +427,11 @@ abstract class TransactionDetailsViewModelBase with Store { String setNewFee({double? value, required TransactionPriority priority}) { newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null - ? bitcoin!.getEstimatedFeeWithFeeRate(wallet, value.round(), transactionInfo.amount) + ? bitcoin!.feeAmountWithFeeRate( + wallet, + value.round(), + transactionInfo.inputAddresses?.length ?? 1, + transactionInfo.outputAddresses?.length ?? 1) : bitcoin!.getFeeAmountForPriority( wallet, priority, diff --git a/tool/configure.dart b/tool/configure.dart index 32b470979..8b5af92b2 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -210,6 +210,7 @@ abstract class Bitcoin { int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size}); int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, {int? outputsCount, int? size}); + int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, {int? size}); int getHeightByDate({required DateTime date}); Future rescan(Object wallet, {required int height, bool? doSingleScan}); Future getNodeIsElectrsSPEnabled(Object wallet); From 9c29dbd6fd7823e4deb5da871b752087ca8ef06f Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 10 Aug 2024 01:18:55 +0300 Subject: [PATCH 015/142] fix zero initial fee rates in RBF rate picker (#1585) * fix zero initial fee rates in RBF rate picker * fix for other settings page[skip ci] --- lib/di.dart | 4 ++-- lib/src/screens/settings/other_settings_page.dart | 7 ++++++- .../transaction_details/transaction_details_page.dart | 7 ++++++- lib/src/widgets/picker.dart | 4 ++-- lib/view_model/settings/other_settings_view_model.dart | 4 +++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index a37574f21..a64270f6d 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -759,8 +759,8 @@ Future setup({ getIt.registerFactory(() => TrocadorProvidersViewModel(getIt.get())); getIt.registerFactory(() { - return OtherSettingsViewModel(getIt.get(), getIt.get().wallet!); - }); + return OtherSettingsViewModel(getIt.get(), getIt.get().wallet!, + getIt.get());}); getIt.registerFactory(() { return SecuritySettingsViewModel(getIt.get()); diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 9bba51944..137f699f5 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -12,7 +13,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class OtherSettingsPage extends BasePage { - OtherSettingsPage(this._otherSettingsViewModel); + OtherSettingsPage(this._otherSettingsViewModel) { + if (_otherSettingsViewModel.sendViewModel.isElectrumWallet) { + bitcoin!.updateFeeRates(_otherSettingsViewModel.sendViewModel.wallet); + } + } @override String get title => S.current.other_settings; diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 7734f37ed..d06b935dd 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; @@ -15,7 +16,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class TransactionDetailsPage extends BasePage { - TransactionDetailsPage({required this.transactionDetailsViewModel}); + TransactionDetailsPage({required this.transactionDetailsViewModel}) { + if (transactionDetailsViewModel.sendViewModel.isElectrumWallet) { + bitcoin!.updateFeeRates(transactionDetailsViewModel.sendViewModel.wallet); + } + } @override String get title => S.current.transaction_details_title; diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index b744d1db0..a7cb03a4e 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -499,10 +499,10 @@ class _PickerState extends State> { children: [ Expanded( child: Slider( - value: widget.sliderValue ?? 1, + value: widget.sliderValue == null || widget.sliderValue! < 1 ? 1 : widget.sliderValue!, onChanged: isActivated ? widget.onSliderChanged : null, min: widget.minValue ?? 1, - max: widget.maxValue ?? 100, + max: (widget.maxValue == null || widget.maxValue! < 1) ? 100 : widget.maxValue!, divisions: 100, ), ), diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index bd04755fa..9af8c67cf 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/package_info.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; // import 'package:package_info/package_info.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/balance.dart'; @@ -20,7 +21,7 @@ class OtherSettingsViewModel = OtherSettingsViewModelBase with _$OtherSettingsViewModel; abstract class OtherSettingsViewModelBase with Store { - OtherSettingsViewModelBase(this._settingsStore, this._wallet) + OtherSettingsViewModelBase(this._settingsStore, this._wallet, this.sendViewModel) : walletType = _wallet.type, currentVersion = '' { PackageInfo.fromPlatform().then( @@ -42,6 +43,7 @@ abstract class OtherSettingsViewModelBase with Store { String currentVersion; final SettingsStore _settingsStore; + final SendViewModel sendViewModel; @computed TransactionPriority get transactionPriority { From b412d45f0e3a97758aa5b438c90979f2a39bb324 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 10 Aug 2024 01:21:26 +0300 Subject: [PATCH 016/142] Cw 567 cant swipe through menus on desktop builds (#1563) * MaterialApp scrollBehavior * accessibility improvements --- lib/app_scroll_behavior.dart | 9 +++++ lib/main.dart | 3 ++ lib/src/screens/dashboard/dashboard_page.dart | 6 +++- .../screens/dashboard/pages/balance_page.dart | 23 +++++++----- .../screens/restore/wallet_restore_page.dart | 26 ++++++++------ lib/src/screens/send/send_page.dart | 9 +++-- lib/src/screens/send/send_template_page.dart | 36 +++++++++++-------- 7 files changed, 75 insertions(+), 37 deletions(-) create mode 100644 lib/app_scroll_behavior.dart diff --git a/lib/app_scroll_behavior.dart b/lib/app_scroll_behavior.dart new file mode 100644 index 000000000..d5abd5688 --- /dev/null +++ b/lib/app_scroll_behavior.dart @@ -0,0 +1,9 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class AppScrollBehavior extends MaterialScrollBehavior { + @override + Set get dragDevices => + {PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.trackpad}; +} diff --git a/lib/main.dart b/lib/main.dart index 014d5f011..1c0078e16 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/app_scroll_behavior.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/di.dart'; @@ -75,6 +76,7 @@ Future main() async { runApp( MaterialApp( debugShowCheckedModeBanner: false, + scrollBehavior: AppScrollBehavior(), home: Scaffold( body: SingleChildScrollView( child: Container( @@ -297,6 +299,7 @@ class AppState extends State with SingleTickerProviderStateMixin { locale: Locale(settingsStore.languageCode), onGenerateRoute: (settings) => Router.createRoute(settings), initialRoute: initialRoute, + scrollBehavior: AppScrollBehavior(), home: _Home(), )); }); diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 7a2055930..ad6e68cd8 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -237,7 +237,11 @@ class _DashboardPageView extends BasePage { padding: EdgeInsets.only(bottom: 24, top: 10), child: Observer( builder: (context) { - return ExcludeSemantics( + return Semantics( + button: false, + label: 'Page Indicator', + hint: 'Swipe to change page', + excludeSemantics: true, child: SmoothPageIndicator( controller: controller, count: pages.length, diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 1cf3e3e0c..770cda6f9 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -23,6 +23,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -469,15 +470,19 @@ class BalanceRowWidget extends StatelessWidget { children: [ Row( children: [ - Text('${availableBalanceLabel}', - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1)), + Semantics( + hint: 'Double tap to see more information', + container: true, + child: Text('${availableBalanceLabel}', + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1)), + ), if (hasAdditionalBalance) Padding( padding: const EdgeInsets.symmetric(horizontal: 4), diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index a9bd52b26..746b73dca 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -177,16 +177,22 @@ class WalletRestorePage extends BasePage { if (_pages.length > 1) Padding( padding: EdgeInsets.only(top: 10), - child: SmoothPageIndicator( - controller: _controller, - count: _pages.length, - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).hintColor.withOpacity(0.5), - activeDotColor: Theme.of(context).hintColor, + child: Semantics( + button: false, + label: 'Page Indicator', + hint: 'Swipe to change restore mode', + excludeSemantics: true, + child: SmoothPageIndicator( + controller: _controller, + count: _pages.length, + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).hintColor.withOpacity(0.5), + activeDotColor: Theme.of(context).hintColor, + ), ), ), ), diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index b46a7f3db..97a7ad88d 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -212,7 +212,12 @@ class SendPage extends BasePage { final count = sendViewModel.outputs.length; return count > 1 - ? SmoothPageIndicator( + ? Semantics ( + label: 'Page Indicator', + hint: 'Swipe to change receiver', + excludeSemantics: true, + child: + SmoothPageIndicator( controller: controller, count: count, effect: ScrollingDotsEffect( @@ -226,7 +231,7 @@ class SendPage extends BasePage { activeDotColor: Theme.of(context) .extension()! .templateBackgroundColor), - ) + )) : Offstage(); }, ), diff --git a/lib/src/screens/send/send_template_page.dart b/lib/src/screens/send/send_template_page.dart index f7c9da082..5db70c0eb 100644 --- a/lib/src/screens/send/send_template_page.dart +++ b/lib/src/screens/send/send_template_page.dart @@ -94,21 +94,27 @@ class SendTemplatePage extends BasePage { final count = sendTemplateViewModel.recipients.length; return count > 1 - ? SmoothPageIndicator( - controller: controller, - count: count, - effect: ScrollingDotsEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context) - .extension()! - .indicatorDotColor, - activeDotColor: Theme.of(context) - .extension()! - .indicatorDotTheme - .activeIndicatorColor)) + ? Semantics( + button: false, + label: 'Page Indicator', + hint: 'Swipe to change receiver', + excludeSemantics: true, + child: SmoothPageIndicator( + controller: controller, + count: count, + effect: ScrollingDotsEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context) + .extension()! + .indicatorDotColor, + activeDotColor: Theme.of(context) + .extension()! + .indicatorDotTheme + .activeIndicatorColor)), + ) : Offstage(); }, ), From 96baf460f3492d1d5e4320b581bae79180b33790 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:02:47 +0100 Subject: [PATCH 017/142] Filters out TRC10 spam transactions and modifies Solana error messages (#1587) * fix: Tron and solana fixes * fix: Disable send all for solana wallets * fix: Add localization and add tostring to get more info on error * fix: Fix spelling for comment --------- Co-authored-by: Omar Hatem --- cw_solana/lib/solana_client.dart | 2 +- cw_solana/pubspec.yaml | 2 +- cw_tron/lib/tron_wallet.dart | 5 +++++ lib/src/screens/send/widgets/send_card.dart | 2 +- lib/view_model/send/send_view_model.dart | 10 ++++++++-- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + 31 files changed, 42 insertions(+), 5 deletions(-) diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 38f2864df..23e88fe5e 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -456,7 +456,7 @@ class SolanaWalletClient { funder: ownerKeypair, ); } catch (e) { - throw Exception('Insufficient SOL balance to complete this transaction'); + throw Exception('Insufficient SOL balance to complete this transaction: ${e.toString()}'); } // Input by the user diff --git a/cw_solana/pubspec.yaml b/cw_solana/pubspec.yaml index 6b59282b4..6fd5cd97c 100644 --- a/cw_solana/pubspec.yaml +++ b/cw_solana/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - solana: ^0.30.1 + solana: ^0.30.4 cw_core: path: ../cw_core http: ^1.1.0 diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index cb4c9c024..3566dcd94 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -349,6 +349,11 @@ abstract class TronWalletBase continue; } + // Filter out spam transaactions that involve receiving TRC10 assets transaction, we deal with TRX and TRC20 transactions + if (transactionModel.contracts?.first.type == "TransferAssetContract") { + continue; + } + String? tokenSymbol; if (transactionModel.contractAddress != null) { final tokenAddress = TronAddress(transactionModel.contractAddress!); diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index e2e7f25da..0a3de3e58 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -216,7 +216,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin _presentPicker(context), isPickerEnable: sendViewModel.hasMultipleTokens, tag: sendViewModel.selectedCryptoCurrency.tag, - allAmountButton: !sendViewModel.isBatchSending, + allAmountButton: !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, currencyValueValidator: output.sendAll ? sendViewModel.allAmountValidator : sendViewModel.amountValidator, diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index a1997e81d..d0514bb19 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -118,7 +118,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed bool get isBatchSending => outputs.length > 1; - bool get shouldDisplaySendALL => walletType != WalletType.solana || walletType != WalletType.tron; + bool get shouldDisplaySendALL => walletType != WalletType.solana; @computed String get pendingTransactionFiatAmount { @@ -582,9 +582,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ) { String errorMessage = error.toString(); + if (walletType == WalletType.solana) { + if (errorMessage.contains('insufficient funds for rent')) { + return S.current.insufficientFundsForRentError; + } + + return errorMessage; + } if (walletType == WalletType.ethereum || walletType == WalletType.polygon || - walletType == WalletType.solana || walletType == WalletType.haven) { if (errorMessage.contains('gas required exceeds allowance') || errorMessage.contains('insufficient funds')) { diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 435c7ccde..d543706fc 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -337,6 +337,7 @@ "incoming": "الواردة", "incorrect_seed": "النص الذي تم إدخاله غير صالح.", "inputs": "المدخلات", + "insufficientFundsForRentError": "ليس لديك ما يكفي من SOL لتغطية رسوم المعاملة والإيجار للحساب. يرجى إضافة المزيد من sol إلى محفظتك أو تقليل مبلغ sol الذي ترسله", "introducing_cake_pay": "نقدم لكم Cake Pay!", "invalid_input": "مدخل غير صالح", "invoice_details": "تفاصيل الفاتورة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 84bcabf9f..ede60567d 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -337,6 +337,7 @@ "incoming": "Входящи", "incorrect_seed": "Въведеният текст е невалиден.", "inputs": "Входове", + "insufficientFundsForRentError": "Нямате достатъчно SOL, за да покриете таксата за транзакцията и наемането на сметката. Моля, добавете повече SOL към портфейла си или намалете сумата на SOL, която изпращате", "introducing_cake_pay": "Запознайте се с Cake Pay!", "invalid_input": "Невалиден вход", "invoice_details": "IДанни за фактура", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 731513bd7..8f2cda1e0 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -337,6 +337,7 @@ "incoming": "Příchozí", "incorrect_seed": "Zadaný text není správný.", "inputs": "Vstupy", + "insufficientFundsForRentError": "Nemáte dostatek SOL na pokrytí transakčního poplatku a nájemného za účet. Laskavě přidejte do své peněženky více SOL nebo snižte množství Sol, kterou odesíláte", "introducing_cake_pay": "Představujeme Cake Pay!", "invalid_input": "Neplatný vstup", "invoice_details": "detaily faktury", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 0f43e831d..59a23222c 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -337,6 +337,7 @@ "incoming": "Eingehend", "incorrect_seed": "Der eingegebene Text ist ungültig.", "inputs": "Eingänge", + "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Brieftasche hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", "introducing_cake_pay": "Einführung von Cake Pay!", "invalid_input": "Ungültige Eingabe", "invoice_details": "Rechnungs-Details", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index e4ef2119d..1bd3fc241 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -337,6 +337,7 @@ "incoming": "Incoming", "incorrect_seed": "The text entered is not valid.", "inputs": "Inputs", + "insufficientFundsForRentError": "You do not have enough SOL to cover the transaction fee and rent for the account. Kindly add more SOL to your wallet or reduce the SOL amount you\\'re sending", "introducing_cake_pay": "Introducing Cake Pay!", "invalid_input": "Invalid input", "invoice_details": "Invoice details", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 0b7dc4a28..dc8aa3b95 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -337,6 +337,7 @@ "incoming": "Entrante", "incorrect_seed": "El texto ingresado no es válido.", "inputs": "Entradas", + "insufficientFundsForRentError": "No tiene suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando", "introducing_cake_pay": "¡Presentamos Cake Pay!", "invalid_input": "Entrada inválida", "invoice_details": "Detalles de la factura", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index fad9576e8..f7c45f7ef 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -337,6 +337,7 @@ "incoming": "Entrantes", "incorrect_seed": "Le texte entré est invalide.", "inputs": "Contributions", + "insufficientFundsForRentError": "Vous n'avez pas assez de SOL pour couvrir les frais de transaction et le loyer pour le compte. Veuillez ajouter plus de Sol à votre portefeuille ou réduire la quantité de sol que vous envoyez", "introducing_cake_pay": "Présentation de Cake Pay !", "invalid_input": "Entrée invalide", "invoice_details": "Détails de la facture", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index de963a52d..a5805bbb8 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -337,6 +337,7 @@ "incoming": "Mai shigowa", "incorrect_seed": "rubutun da aka shigar ba shi da inganci.", "inputs": "Abubuwan da ke ciki", + "insufficientFundsForRentError": "Ba ku da isasshen Sol don rufe kuɗin ma'amala da haya don asusun. Da kyau ƙara ƙarin sool zuwa walat ɗinku ko rage adadin Sol ɗin da kuke aikawa", "introducing_cake_pay": "Gabatar da Cake Pay!", "invalid_input": "Shigar da ba daidai ba", "invoice_details": "Bayanin wadannan", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index edc301efe..4ab8e7534 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -337,6 +337,7 @@ "incoming": "आने वाली", "incorrect_seed": "दर्ज किया गया पाठ मान्य नहीं है।", "inputs": "इनपुट", + "insufficientFundsForRentError": "आपके पास लेन -देन शुल्क और खाते के लिए किराए को कवर करने के लिए पर्याप्त सोल नहीं है। कृपया अपने बटुए में अधिक सोल जोड़ें या सोल राशि को कम करें जिसे आप भेज रहे हैं", "introducing_cake_pay": "परिचय Cake Pay!", "invalid_input": "अमान्य निवेश", "invoice_details": "चालान विवरण", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 03dfcd2a1..67095ba8f 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -337,6 +337,7 @@ "incoming": "Dolazno", "incorrect_seed": "Uneseni tekst nije valjan.", "inputs": "Unosi", + "insufficientFundsForRentError": "Nemate dovoljno SOL -a za pokrivanje naknade za transakciju i najamninu za račun. Ljubazno dodajte više sol u svoj novčanik ili smanjite količinu SOL -a koju šaljete", "introducing_cake_pay": "Predstavljamo Cake Pay!", "invalid_input": "Pogrešan unos", "invoice_details": "Podaci o fakturi", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index f3276ff6c..939b938fe 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -337,6 +337,7 @@ "incoming": "Masuk", "incorrect_seed": "Teks yang dimasukkan tidak valid.", "inputs": "Input", + "insufficientFundsForRentError": "Anda tidak memiliki cukup SOL untuk menutupi biaya transaksi dan menyewa untuk akun tersebut. Mohon tambahkan lebih banyak sol ke dompet Anda atau kurangi jumlah sol yang Anda kirim", "introducing_cake_pay": "Perkenalkan Cake Pay!", "invalid_input": "Masukan tidak valid", "invoice_details": "Detail faktur", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index d021e37a3..29a142d1e 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -338,6 +338,7 @@ "incoming": "In arrivo", "incorrect_seed": "Il testo inserito non è valido.", "inputs": "Input", + "insufficientFundsForRentError": "Non hai abbastanza SOL per coprire la tassa di transazione e l'affitto per il conto. Si prega di aggiungere più SOL al tuo portafoglio o ridurre l'importo SOL che stai inviando", "introducing_cake_pay": "Presentazione di Cake Pay!", "invalid_input": "Inserimento non valido", "invoice_details": "Dettagli della fattura", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index dd28f9688..3009aa115 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -338,6 +338,7 @@ "incoming": "着信", "incorrect_seed": "入力されたテキストは無効です。", "inputs": "入力", + "insufficientFundsForRentError": "アカウントの取引料金とレンタルをカバーするのに十分なソルがありません。財布にソルを追加するか、送信するソル量を減らしてください", "introducing_cake_pay": "序章Cake Pay!", "invalid_input": "無効入力", "invoice_details": "請求の詳細", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 8b86c04c6..53b3cc875 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -337,6 +337,7 @@ "incoming": "들어오는", "incorrect_seed": "입력하신 텍스트가 유효하지 않습니다.", "inputs": "입력", + "insufficientFundsForRentError": "거래 수수료와 계좌 임대료를 충당하기에 충분한 SOL이 없습니다. 지갑에 더 많은 솔을 추가하거나 보내는 솔을 줄이십시오.", "introducing_cake_pay": "소개 Cake Pay!", "invalid_input": "잘못된 입력", "invoice_details": "인보이스 세부정보", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 42eb54f21..64a7a1ad1 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -337,6 +337,7 @@ "incoming": "ဝင်လာ", "incorrect_seed": "ထည့်သွင်းထားသော စာသားသည် မမှန်ကန်ပါ။", "inputs": "သွင်းငေှ", + "insufficientFundsForRentError": "သင်ငွေပေးချေမှုအခကြေးငွေကိုဖုံးအုပ်ရန်နှင့်အကောင့်ငှားရန်လုံလောက်သော sol ရှိသည်မဟုတ်ကြဘူး။ ကြင်နာစွာသင်၏ပိုက်ဆံအိတ်သို့ပိုမို sol ကိုပိုမိုထည့်ပါသို့မဟုတ်သင်ပို့ခြင်း sol ပမာဏကိုလျှော့ချပါ", "introducing_cake_pay": "Cake Pay ကို မိတ်ဆက်ခြင်း။", "invalid_input": "ထည့်သွင်းမှု မမှန်ကန်ပါ။", "invoice_details": "ပြေစာအသေးစိတ်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 53db56332..86f6b8c0b 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -337,6 +337,7 @@ "incoming": "inkomend", "incorrect_seed": "De ingevoerde tekst is niet geldig.", "inputs": "Invoer", + "insufficientFundsForRentError": "U hebt niet genoeg SOL om de transactiekosten en huur voor de rekening te dekken. Voeg vriendelijk meer SOL toe aan uw portemonnee of verminder de SOL -hoeveelheid die u verzendt", "introducing_cake_pay": "Introductie van Cake Pay!", "invalid_input": "Ongeldige invoer", "invoice_details": "Factuurgegevens", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 898756982..34a8d57fe 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -337,6 +337,7 @@ "incoming": "Przychodzące", "incorrect_seed": "Wprowadzony seed jest nieprawidłowy.", "inputs": "Wejścia", + "insufficientFundsForRentError": "Nie masz wystarczającej ilości SOL, aby pokryć opłatę za transakcję i czynsz za konto. Uprzejmie dodaj więcej sol do portfela lub zmniejsz solę, którą wysyłasz", "introducing_cake_pay": "Przedstawiamy Cake Pay!", "invalid_input": "Nieprawidłowe dane wejściowe", "invoice_details": "Dane do faktury", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 9e99866cb..67d68988f 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -337,6 +337,7 @@ "incoming": "Recebidas", "incorrect_seed": "O texto digitado não é válido.", "inputs": "Entradas", + "insufficientFundsForRentError": "Você não tem Sol suficiente para cobrir a taxa de transação e o aluguel da conta. Por favor, adicione mais sol à sua carteira ou reduza a quantidade de sol que você envia", "introducing_cake_pay": "Apresentando o Cake Pay!", "invalid_input": "Entrada inválida", "invoice_details": "Detalhes da fatura", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 8d3892ace..521cda83d 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -337,6 +337,7 @@ "incoming": "Входящие", "incorrect_seed": "Введённый текст некорректный.", "inputs": "Входы", + "insufficientFundsForRentError": "У вас недостаточно Sol, чтобы покрыть плату за транзакцию и аренду для счета. Пожалуйста, добавьте больше Sol в свой кошелек или уменьшите сумму Sol, которую вы отправляете", "introducing_cake_pay": "Представляем Cake Pay!", "invalid_input": "Неверный Ввод", "invoice_details": "Детали счета", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 9db9caa68..996472f47 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -337,6 +337,7 @@ "incoming": "ขาเข้า", "incorrect_seed": "ข้อความที่ป้อนไม่ถูกต้อง", "inputs": "อินพุต", + "insufficientFundsForRentError": "คุณไม่มีโซลเพียงพอที่จะครอบคลุมค่าธรรมเนียมการทำธุรกรรมและค่าเช่าสำหรับบัญชี กรุณาเพิ่มโซลให้มากขึ้นลงในกระเป๋าเงินของคุณหรือลดจำนวนโซลที่คุณส่งมา", "introducing_cake_pay": "ยินดีต้อนรับสู่ Cake Pay!", "invalid_input": "อินพุตไม่ถูกต้อง", "invoice_details": "รายละเอียดใบแจ้งหนี้", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 0f77f4813..27e4974bb 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -337,6 +337,7 @@ "incoming": "Papasok", "incorrect_seed": "Ang teksto na ipinasok ay hindi wasto.", "inputs": "Mga input", + "insufficientFundsForRentError": "Wala kang sapat na sol upang masakop ang bayad sa transaksyon at upa para sa account. Mabait na magdagdag ng higit pa sa iyong pitaka o bawasan ang halaga ng sol na iyong ipinapadala", "introducing_cake_pay": "Ipinakikilala ang cake pay!", "invalid_input": "Di -wastong input", "invoice_details": "Mga detalye ng invoice", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 961f0cf77..74b72581e 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -337,6 +337,7 @@ "incoming": "Gelen", "incorrect_seed": "Girilen metin geçerli değil.", "inputs": "Girişler", + "insufficientFundsForRentError": "İşlem ücretini karşılamak ve hesap için kiralamak için yeterli SOL'nuz yok. Lütfen cüzdanınıza daha fazla sol ekleyin veya gönderdiğiniz sol miktarını azaltın", "introducing_cake_pay": "Cake Pay ile tanışın!", "invalid_input": "Geçersiz Giriş", "invoice_details": "fatura detayları", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index c9cda0854..74b2e4703 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -337,6 +337,7 @@ "incoming": "Вхідні", "incorrect_seed": "Введений текст невірний.", "inputs": "Вхoди", + "insufficientFundsForRentError": "У вас недостатньо SOL, щоб покрити плату за транзакцію та оренду на рахунок. Будь ласка, додайте до свого гаманця більше SOL або зменшіть суму, яку ви надсилаєте", "introducing_cake_pay": "Представляємо Cake Pay!", "invalid_input": "Неправильні дані", "invoice_details": "Реквізити рахунку-фактури", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 43cab6370..35d024188 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -337,6 +337,7 @@ "incoming": "آنے والا", "incorrect_seed": "درج کردہ متن درست نہیں ہے۔", "inputs": "آدانوں", + "insufficientFundsForRentError": "آپ کے پاس ٹرانزیکشن فیس اور اکاؤنٹ کے لئے کرایہ لینے کے ل enough اتنا SOL نہیں ہے۔ برائے مہربانی اپنے بٹوے میں مزید سول شامل کریں یا آپ کو بھیجنے والی سول رقم کو کم کریں", "introducing_cake_pay": "Cake پے کا تعارف!", "invalid_input": "غلط ان پٹ", "invoice_details": "رسید کی تفصیلات", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index b7d8ad828..29b8d9b71 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -338,6 +338,7 @@ "incoming": "Wọ́n tó ń bọ̀", "incorrect_seed": "Ọ̀rọ̀ tí a tẹ̀ kì í ṣe èyí.", "inputs": "Igbewọle", + "insufficientFundsForRentError": "O ko ni Sol kan lati bo owo isanwo naa ki o yalo fun iroyin naa. Fi agbara kun Sol diẹ sii si apamọwọ rẹ tabi dinku soso naa ti o \\ 'tun n firanṣẹ", "introducing_cake_pay": "Ẹ bá Cake Pay!", "invalid_input": "Iṣawọle ti ko tọ", "invoice_details": "Iru awọn ẹya ọrọ", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 389cf6c24..a30acad70 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -337,6 +337,7 @@ "incoming": "收到", "incorrect_seed": "输入的文字无效。", "inputs": "输入", + "insufficientFundsForRentError": "您没有足够的溶胶来支付该帐户的交易费和租金。请在钱包中添加更多溶胶或减少您发送的溶胶量", "introducing_cake_pay": "介绍 Cake Pay!", "invalid_input": "输入无效", "invoice_details": "发票明细", From bbba41396d32b0fde52b6d676ee310c9fa26f639 Mon Sep 17 00:00:00 2001 From: Rafael Date: Sun, 11 Aug 2024 20:49:45 -0300 Subject: [PATCH 018/142] Fixes node connection, and sp, and electrum (#1577) * refactor: remove bitcoin_flutter, update deps, electrs node improvements * feat: connecting/disconnecting improvements, fix rescan by date, scanning message * chore: print * Update pubspec.yaml * Update pubspec.yaml * handle null sockets, retry connection on connect failure * fix imports * fix transaction history * fix RBF * minor fixes/readability enhancements [skip ci] --------- Co-authored-by: Omar Hatem Co-authored-by: Matthew Fosse --- .../lib/bitcoin_hardware_wallet_service.dart | 5 +- cw_bitcoin/lib/bitcoin_wallet.dart | 18 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 5 +- cw_bitcoin/lib/electrum.dart | 111 ++++--- cw_bitcoin/lib/electrum_transaction_info.dart | 4 +- cw_bitcoin/lib/electrum_wallet.dart | 295 +++++++++++------- cw_bitcoin/lib/electrum_wallet_addresses.dart | 30 +- cw_bitcoin/lib/litecoin_network.dart | 9 - cw_bitcoin/lib/litecoin_wallet.dart | 6 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 6 +- cw_bitcoin/lib/utils.dart | 72 ++--- cw_bitcoin/pubspec.lock | 79 ++--- cw_bitcoin/pubspec.yaml | 12 +- .../lib/src/bitcoin_cash_wallet.dart | 26 +- .../src/bitcoin_cash_wallet_addresses.dart | 6 +- .../lib/src/bitcoin_cash_wallet_service.dart | 12 +- cw_bitcoin_cash/lib/src/mnemonic.dart | 2 +- cw_bitcoin_cash/pubspec.yaml | 8 +- cw_core/lib/get_height_by_date.dart | 7 +- cw_core/lib/node.dart | 9 +- cw_core/lib/sync_status.dart | 5 + cw_core/lib/transaction_info.dart | 5 +- cw_haven/pubspec.lock | 16 +- cw_monero/pubspec.lock | 4 +- cw_nano/pubspec.lock | 54 ++-- cw_nano/pubspec.yaml | 1 + cw_tron/pubspec.yaml | 4 +- cw_wownero/pubspec.lock | 20 +- ios/Podfile.lock | 12 +- lib/bitcoin/cw_bitcoin.dart | 50 +-- lib/core/sync_status_title.dart | 4 + .../cake_pay_confirm_purchase_card_page.dart | 48 +-- pubspec_base.yaml | 6 +- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + tool/configure.dart | 3 +- 60 files changed, 525 insertions(+), 455 deletions(-) delete mode 100644 cw_bitcoin/lib/litecoin_network.dart diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index 345d645d1..de339175d 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; @@ -25,7 +25,8 @@ class BitcoinHardwareWalletService { for (final i in indexRange) { final derivationPath = "m/84'/0'/$i'"; final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath); - HDWallet hd = HDWallet.fromBase58(xpub).derive(0); + Bip32Slip10Secp256k1 hd = + Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet); diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index ce3e2caa8..7b8250541 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -2,8 +2,7 @@ import 'dart:convert'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:convert/convert.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; @@ -51,11 +50,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - networkType: networkParam == null - ? bitcoin.bitcoin + network: networkParam == null + ? BitcoinNetwork.mainnet : networkParam == BitcoinNetwork.mainnet - ? bitcoin.bitcoin - : bitcoin.testnet, + ? BitcoinNetwork.mainnet + : BitcoinNetwork.testnet, initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: seedBytes, @@ -76,10 +75,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddresses: initialSilentAddresses, initialSilentAddressIndex: initialSilentAddressIndex, mainHd: hd, - sideHd: accountHD.derive(1), + sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: networkParam ?? network, - masterHd: - seedBytes != null ? bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) : null, + masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, ); autorun((_) { @@ -253,7 +251,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); - return BtcTransaction.fromRaw(hex.encode(rawHex)); + return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex)); } @override diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 486e69b11..697719894 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,5 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart'; +import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/wallet_info.dart'; @@ -24,7 +24,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S }) : super(walletInfo); @override - String getAddress({required int index, required HDWallet hd, BitcoinAddressType? addressType}) { + String getAddress( + {required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) { if (addressType == P2pkhAddressType.p2pkh) return generateP2PKHAddress(hd: hd, index: index, network: network); diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index e3925ca74..69b07d7c1 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -8,6 +8,8 @@ import 'package:cw_bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; +enum ConnectionStatus { connected, disconnected, connecting, failed } + String jsonrpcparams(List params) { final _params = params.map((val) => '"${val.toString()}"').join(','); return '[$_params]'; @@ -41,7 +43,7 @@ class ElectrumClient { bool get isConnected => _isConnected; Socket? socket; - void Function(bool?)? onConnectionStatusChange; + void Function(ConnectionStatus)? onConnectionStatusChange; int _id; final Map _tasks; Map get tasks => _tasks; @@ -60,17 +62,33 @@ class ElectrumClient { } Future connect({required String host, required int port, bool? useSSL}) async { + _setConnectionStatus(ConnectionStatus.connecting); + try { await socket?.close(); } catch (_) {} - if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { - socket = await Socket.connect(host, port, timeout: connectionTimeout); - } else { - socket = await SecureSocket.connect(host, port, - timeout: connectionTimeout, onBadCertificate: (_) => true); + try { + if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { + socket = await Socket.connect(host, port, timeout: connectionTimeout); + } else { + socket = await SecureSocket.connect( + host, + port, + timeout: connectionTimeout, + onBadCertificate: (_) => true, + ); + } + } catch (_) { + _setConnectionStatus(ConnectionStatus.failed); + return; } - _setIsConnected(true); + + if (socket == null) { + _setConnectionStatus(ConnectionStatus.failed); + return; + } + _setConnectionStatus(ConnectionStatus.connected); socket!.listen((Uint8List event) { try { @@ -86,13 +104,20 @@ class ElectrumClient { print(e.toString()); } }, onError: (Object error) { - print(error.toString()); + final errorMsg = error.toString(); + print(errorMsg); unterminatedString = ''; - _setIsConnected(false); + + final currentHost = socket?.address.host; + final isErrorForCurrentHost = errorMsg.contains(" ${currentHost} "); + + if (currentHost != null && isErrorForCurrentHost) + _setConnectionStatus(ConnectionStatus.failed); }, onDone: () { unterminatedString = ''; - _setIsConnected(null); + if (host == socket?.address.host) _setConnectionStatus(ConnectionStatus.disconnected); }); + keepAlive(); } @@ -144,9 +169,9 @@ class ElectrumClient { Future ping() async { try { await callWithTimeout(method: 'server.ping'); - _setIsConnected(true); + _setConnectionStatus(ConnectionStatus.connected); } on RequestFailedTimeoutException catch (_) { - _setIsConnected(null); + _setConnectionStatus(ConnectionStatus.disconnected); } } @@ -236,37 +261,39 @@ class ElectrumClient { return []; }); - Future> getTransactionRaw({required String hash}) async { + Future getTransaction({required String hash, required bool verbose}) async { try { final result = await callWithTimeout( - method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000); + method: 'blockchain.transaction.get', params: [hash, verbose], timeout: 10000); if (result is Map) { return result; } } on RequestFailedTimeoutException catch (_) { return {}; } catch (e) { - print("getTransactionRaw: ${e.toString()}"); + print("getTransaction: ${e.toString()}"); return {}; } return {}; } - Future getTransactionHex({required String hash}) async { - try { - final result = await callWithTimeout( - method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000); - if (result is String) { - return result; - } - } on RequestFailedTimeoutException catch (_) { - return ''; - } catch (e) { - print("getTransactionHex: ${e.toString()}"); - return ''; - } - return ''; - } + Future> getTransactionVerbose({required String hash}) => + getTransaction(hash: hash, verbose: true).then((dynamic result) { + if (result is Map) { + return result; + } + + return {}; + }); + + Future getTransactionHex({required String hash}) => + getTransaction(hash: hash, verbose: false).then((dynamic result) { + if (result is String) { + return result; + } + + return ''; + }); Future broadcastTransaction( {required String transactionRaw, @@ -348,7 +375,7 @@ class ElectrumClient { try { final topDoubleString = await estimatefee(p: 1); final middleDoubleString = await estimatefee(p: 5); - final bottomDoubleString = await estimatefee(p: 100); + final bottomDoubleString = await estimatefee(p: 10); final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round(); final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round(); final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round(); @@ -398,6 +425,10 @@ class ElectrumClient { BehaviorSubject? subscribe( {required String id, required String method, List params = const []}) { try { + if (socket == null) { + _setConnectionStatus(ConnectionStatus.failed); + return null; + } final subscription = BehaviorSubject(); _regisrySubscription(id, subscription); socket!.write(jsonrpc(method: method, id: _id, params: params)); @@ -411,6 +442,10 @@ class ElectrumClient { Future call( {required String method, List params = const [], Function(int)? idCallback}) async { + if (socket == null) { + _setConnectionStatus(ConnectionStatus.failed); + return null; + } final completer = Completer(); _id += 1; final id = _id; @@ -424,6 +459,10 @@ class ElectrumClient { Future callWithTimeout( {required String method, List params = const [], int timeout = 4000}) async { try { + if (socket == null) { + _setConnectionStatus(ConnectionStatus.failed); + return null; + } final completer = Completer(); _id += 1; final id = _id; @@ -445,6 +484,7 @@ class ElectrumClient { _aliveTimer?.cancel(); try { await socket?.close(); + socket = null; } catch (_) {} onConnectionStatusChange = null; } @@ -493,12 +533,9 @@ class ElectrumClient { } } - void _setIsConnected(bool? isConnected) { - if (_isConnected != isConnected) { - onConnectionStatusChange?.call(isConnected); - } - - _isConnected = isConnected ?? false; + void _setConnectionStatus(ConnectionStatus status) { + onConnectionStatusChange?.call(status); + _isConnected = status == ConnectionStatus.connected; } void _handleResponse(Map response) { diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index d06cfe9de..ea4a3de33 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -22,7 +22,7 @@ class ElectrumTransactionInfo extends TransactionInfo { ElectrumTransactionInfo(this.type, {required String id, - required int height, + int? height, required int amount, int? fee, List? inputAddresses, @@ -99,7 +99,7 @@ class ElectrumTransactionInfo extends TransactionInfo { factory ElectrumTransactionInfo.fromElectrumBundle( ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network, - {required Set addresses, required int height}) { + {required Set addresses, int? height}) { final date = bundle.time != null ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) : DateTime.now(); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index e55e5ed0e..e1b038beb 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -5,7 +5,6 @@ import 'dart:isolate'; import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; @@ -22,7 +21,6 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; -import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/utils.dart'; @@ -42,7 +40,6 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; -import 'package:http/http.dart' as http; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; @@ -60,7 +57,7 @@ abstract class ElectrumWalletBase required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, - required this.networkType, + required this.network, String? xpub, String? mnemonic, Uint8List? seedBytes, @@ -71,7 +68,7 @@ abstract class ElectrumWalletBase CryptoCurrency? currency, this.alwaysScan, }) : accountHD = - getAccountHDWallet(currency, networkType, seedBytes, xpub, walletInfo.derivationInfo), + getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -90,8 +87,7 @@ abstract class ElectrumWalletBase } : {}), this.unspentCoinsInfo = unspentCoinsInfo, - this.network = _getNetwork(networkType, currency), - this.isTestnet = networkType == bitcoin.testnet, + this.isTestnet = network == BitcoinNetwork.testnet, this._mnemonic = mnemonic, super(walletInfo) { this.electrumClient = electrumClient ?? ElectrumClient(); @@ -101,12 +97,8 @@ abstract class ElectrumWalletBase reaction((_) => syncStatus, _syncStatusReaction); } - static bitcoin.HDWallet getAccountHDWallet( - CryptoCurrency? currency, - bitcoin.NetworkType networkType, - Uint8List? seedBytes, - String? xpub, - DerivationInfo? derivationInfo) { + static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, + Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { throw Exception( "To create a Wallet you need either a seed or an xpub. This should not happen"); @@ -115,25 +107,26 @@ abstract class ElectrumWalletBase if (seedBytes != null) { return currency == CryptoCurrency.bch ? bitcoinCashHDWallet(seedBytes) - : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) - .derivePath(_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)); + : Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( + _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)) + as Bip32Slip10Secp256k1; } - return bitcoin.HDWallet.fromBase58(xpub!); + return Bip32Slip10Secp256k1.fromExtendedKey(xpub!); } - static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) => - bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'"); + static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; bool? alwaysScan; - final bitcoin.HDWallet accountHD; + final Bip32Slip10Secp256k1 accountHD; final String? _mnemonic; - bitcoin.HDWallet get hd => accountHD.derive(0); + Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); final String? passphrase; @override @@ -165,7 +158,7 @@ abstract class ElectrumWalletBase .map((addr) => scriptHash(addr.address, network: network)) .toList(); - String get xpub => accountHD.base58!; + String get xpub => accountHD.publicKey.toExtended; @override String? get seed => _mnemonic; @@ -174,7 +167,6 @@ abstract class ElectrumWalletBase WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, xPub: xpub, passphrase: passphrase); - bitcoin.NetworkType networkType; BasedUtxoNetwork network; @override @@ -190,24 +182,21 @@ abstract class ElectrumWalletBase bool _isTryingToConnect = false; @action - Future setSilentPaymentsScanning(bool active, bool usingElectrs) async { + Future setSilentPaymentsScanning(bool active) async { silentPaymentsScanningActive = active; if (active) { - syncStatus = AttemptingSyncStatus(); + syncStatus = StartingScanSyncStatus(); final tip = await getUpdatedChainTip(); if (tip == walletInfo.restoreHeight) { syncStatus = SyncedTipSyncStatus(tip); + return; } if (tip > walletInfo.restoreHeight) { - _setListeners( - walletInfo.restoreHeight, - chainTipParam: _currentChainTip, - usingElectrs: usingElectrs, - ); + _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); } } else { alwaysScan = false; @@ -245,8 +234,11 @@ abstract class ElectrumWalletBase } @override - BitcoinWalletKeys get keys => - BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); + BitcoinWalletKeys get keys => BitcoinWalletKeys( + wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer), + privateKey: hd.privateKey.toHex(), + publicKey: hd.publicKey.toHex(), + ); String _password; List unspentCoins; @@ -278,7 +270,7 @@ abstract class ElectrumWalletBase int height, { int? chainTipParam, bool? doSingleScan, - bool? usingElectrs, + bool? usingSupportedNode, }) async { final chainTip = chainTipParam ?? await getUpdatedChainTip(); @@ -287,7 +279,7 @@ abstract class ElectrumWalletBase return; } - syncStatus = AttemptingSyncStatus(); + syncStatus = StartingScanSyncStatus(); if (_isolate != null) { final runningIsolate = await _isolate!; @@ -305,7 +297,9 @@ abstract class ElectrumWalletBase chainTip: chainTip, electrumClient: ElectrumClient(), transactionHistoryIds: transactionHistory.transactions.keys.toList(), - node: usingElectrs == true ? ScanNode(node!.uri, node!.useSSL) : null, + node: (await getNodeSupportsSilentPayments()) == true + ? ScanNode(node!.uri, node!.useSSL) + : null, labels: walletAddresses.labels, labelIndexes: walletAddresses.silentAddresses .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1) @@ -393,7 +387,7 @@ abstract class ElectrumWalletBase BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)), ) : silentAddress.B_spend, - hrp: silentAddress.hrp, + network: network, ); final addressRecord = walletAddresses.silentAddresses @@ -422,8 +416,6 @@ abstract class ElectrumWalletBase await updateAllUnspents(); await updateBalance(); - Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); - if (alwaysScan == true) { _setListeners(walletInfo.restoreHeight); } else { @@ -446,6 +438,58 @@ abstract class ElectrumWalletBase Node? node; + Future getNodeIsElectrs() async { + if (node == null) { + return false; + } + + final version = await electrumClient.version(); + + if (version.isNotEmpty) { + final server = version[0]; + + if (server.toLowerCase().contains('electrs')) { + node!.isElectrs = true; + node!.save(); + return node!.isElectrs!; + } + } + + + node!.isElectrs = false; + node!.save(); + return node!.isElectrs!; + } + + Future getNodeSupportsSilentPayments() async { + // As of today (august 2024), only ElectrumRS supports silent payments + if (!(await getNodeIsElectrs())) { + return false; + } + + if (node == null) { + return false; + } + + try { + final tweaksResponse = await electrumClient.getTweaks(height: 0); + + if (tweaksResponse != null) { + node!.supportsSilentPayments = true; + node!.save(); + return node!.supportsSilentPayments!; + } + } on RequestFailedTimeoutException catch (_) { + node!.supportsSilentPayments = false; + node!.save(); + return node!.supportsSilentPayments!; + } catch (_) {} + + node!.supportsSilentPayments = false; + node!.save(); + return node!.supportsSilentPayments!; + } + @action @override Future connectToNode({required Node node}) async { @@ -507,13 +551,6 @@ abstract class ElectrumWalletBase final hd = utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; - final derivationPath = - "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" - "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" - "/${utx.bitcoinAddressRecord.index}"; - final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!; - - publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; @@ -530,6 +567,7 @@ abstract class ElectrumWalletBase } vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); + String pubKeyHex; if (privkey != null) { inputPrivKeyInfos.add(ECPrivateInfo( @@ -537,8 +575,18 @@ abstract class ElectrumWalletBase address.type == SegwitAddresType.p2tr, tweak: !isSilentPayment, )); + + pubKeyHex = privkey.getPublic().toHex(); + } else { + pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex(); } + final derivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" + "/${utx.bitcoinAddressRecord.index}"; + publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); + utxos.add( UtxoWithAddress( utxo: BitcoinUtxo( @@ -1127,10 +1175,9 @@ abstract class ElectrumWalletBase int? chainTip, ScanData? scanData, bool? doSingleScan, - bool? usingElectrs, }) async { silentPaymentsScanningActive = true; - _setListeners(height, doSingleScan: doSingleScan, usingElectrs: usingElectrs); + _setListeners(height, doSingleScan: doSingleScan); } @override @@ -1228,7 +1275,7 @@ abstract class ElectrumWalletBase await Future.wait(unspents.map((unspent) async { try { final coin = BitcoinUnspent.fromJSON(address, unspent); - final tx = await fetchTransactionInfo(hash: coin.hash, height: 0); + final tx = await fetchTransactionInfo(hash: coin.hash); coin.isChange = address.isHidden; coin.confirmations = tx?.confirmations; @@ -1283,9 +1330,17 @@ abstract class ElectrumWalletBase } Future canReplaceByFee(String hash) async { - final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); - final confirmations = verboseTransaction['confirmations'] as int? ?? 0; - final transactionHex = verboseTransaction['hex'] as String?; + final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash); + + final String? transactionHex; + int confirmations = 0; + + if (verboseTransaction.isEmpty) { + transactionHex = await electrumClient.getTransactionHex(hash: hash); + } else { + confirmations = verboseTransaction['confirmations'] as int? ?? 0; + transactionHex = verboseTransaction['hex'] as String?; + } if (confirmations > 0) return false; @@ -1293,10 +1348,7 @@ abstract class ElectrumWalletBase return false; } - final original = bitcoin.Transaction.fromHex(transactionHex); - - return original.ins - .any((element) => element.sequence != null && element.sequence! < 4294967293); + return BtcTransaction.fromRaw(transactionHex).canReplaceByFee; } Future isChangeSufficientForFee(String txId, int newFee) async { @@ -1455,50 +1507,73 @@ abstract class ElectrumWalletBase } } - Future getTransactionExpanded({required String hash}) async { + Future getTransactionExpanded( + {required String hash, int? height}) async { String transactionHex; + // TODO: time is not always available, and calculating it from height is not always accurate. + // Add settings to choose API provider and use and http server instead of electrum for this. int? time; - int confirmations = 0; - if (network == BitcoinNetwork.testnet) { - // Testnet public electrum server does not support verbose transaction fetching + int? confirmations; + + final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash); + + if (verboseTransaction.isEmpty) { transactionHex = await electrumClient.getTransactionHex(hash: hash); - - final status = json.decode( - (await http.get(Uri.parse("https://blockstream.info/testnet/api/tx/$hash/status"))).body); - - time = status["block_time"] as int?; - final height = status["block_height"] as int? ?? 0; - final tip = await getUpdatedChainTip(); - if (tip > 0) confirmations = height > 0 ? tip - height + 1 : 0; } else { - final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); - transactionHex = verboseTransaction['hex'] as String; time = verboseTransaction['time'] as int?; - confirmations = verboseTransaction['confirmations'] as int? ?? 0; + confirmations = verboseTransaction['confirmations'] as int?; + } + + if (height != null) { + if (time == null) { + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); + } + + if (confirmations == null) { + final tip = await getUpdatedChainTip(); + if (tip > 0 && height > 0) { + // Add one because the block itself is the first confirmation + confirmations = tip - height + 1; + } + } } final original = BtcTransaction.fromRaw(transactionHex); final ins = []; for (final vin in original.inputs) { - ins.add(BtcTransaction.fromRaw(await electrumClient.getTransactionHex(hash: vin.txId))); + final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId); + + final String inputTransactionHex; + + if (verboseTransaction.isEmpty) { + inputTransactionHex = await electrumClient.getTransactionHex(hash: hash); + } else { + inputTransactionHex = verboseTransaction['hex'] as String; + } + + ins.add(BtcTransaction.fromRaw(inputTransactionHex)); } return ElectrumTransactionBundle( original, ins: ins, time: time, - confirmations: confirmations, + confirmations: confirmations ?? 0, ); } Future fetchTransactionInfo( - {required String hash, required int height, bool? retryOnFailure}) async { + {required String hash, int? height, bool? retryOnFailure}) async { try { return ElectrumTransactionInfo.fromElectrumBundle( - await getTransactionExpanded(hash: hash), walletInfo.type, network, - addresses: addressesSet, height: height); + await getTransactionExpanded(hash: hash, height: height), + walletInfo.type, + network, + addresses: addressesSet, + height: height, + ); } catch (e) { if (e is FormatException && retryOnFailure == true) { await Future.delayed(const Duration(seconds: 2)); @@ -1649,8 +1724,8 @@ abstract class ElectrumWalletBase await getCurrentChainTip(); transactionHistory.transactions.values.forEach((tx) async { - if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height > 0) { - tx.confirmations = await getCurrentChainTip() - tx.height + 1; + if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) { + tx.confirmations = await getCurrentChainTip() - tx.height! + 1; } }); @@ -1766,8 +1841,12 @@ abstract class ElectrumWalletBase final index = address != null ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index : null; - final HD = index == null ? hd : hd.derive(index); - return base64Encode(HD.signMessage(message)); + final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index)); + final priv = ECPrivate.fromWif( + WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer), + netVersion: network.wifNetVer, + ); + return priv.signMessage(StringUtils.encode(message)); } Future _setInitialHeight() async { @@ -1793,43 +1872,42 @@ abstract class ElectrumWalletBase }); } - static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) { - if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) { - return BitcoinCashNetwork.mainnet; - } - - if (networkType == litecoinNetwork) { - return LitecoinNetwork.mainnet; - } - - if (networkType == bitcoin.testnet) { - return BitcoinNetwork.testnet; - } - - return BitcoinNetwork.mainnet; - } - static String _hardenedDerivationPath(String derivationPath) => derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); @action - void _onConnectionStatusChange(bool? isConnected) { - if (syncStatus is SyncingSyncStatus) return; + void _onConnectionStatusChange(ConnectionStatus status) { + switch (status) { + case ConnectionStatus.connected: + if (syncStatus is NotConnectedSyncStatus || + syncStatus is LostConnectionSyncStatus || + syncStatus is ConnectingSyncStatus) { + syncStatus = AttemptingSyncStatus(); + startSync(); + } - if (isConnected == true && syncStatus is! SyncedSyncStatus) { - syncStatus = ConnectedSyncStatus(); - } else if (isConnected == false) { - syncStatus = LostConnectionSyncStatus(); - } else if (isConnected != true && syncStatus is! ConnectingSyncStatus) { - syncStatus = NotConnectedSyncStatus(); + break; + case ConnectionStatus.disconnected: + syncStatus = NotConnectedSyncStatus(); + break; + case ConnectionStatus.failed: + syncStatus = LostConnectionSyncStatus(); + // wait for 5 seconds and then try to reconnect: + Future.delayed(Duration(seconds: 5), () { + electrumClient.connectToUri( + node!.uri, + useSSL: node!.useSSL ?? false, + ); + }); + break; + case ConnectionStatus.connecting: + syncStatus = ConnectingSyncStatus(); + break; + default: } } void _syncStatusReaction(SyncStatus syncStatus) async { - if (syncStatus is! AttemptingSyncStatus && syncStatus is! SyncedTipSyncStatus) { - silentPaymentsScanningActive = syncStatus is SyncingSyncStatus; - } - if (syncStatus is NotConnectedSyncStatus) { // Needs to re-subscribe to all scripthashes when reconnected _scripthashesUpdateSubject = {}; @@ -1950,8 +2028,8 @@ Future startRefresh(ScanData scanData) async { final tweaks = t as Map; if (tweaks["message"] != null) { - // re-subscribe to continue receiving messages - electrumClient.tweaksSubscribe(height: syncHeight, count: count); + // re-subscribe to continue receiving messages, starting from the next unscanned height + electrumClient.tweaksSubscribe(height: syncHeight + 1, count: count); return; } @@ -2180,3 +2258,4 @@ class UtxoDetails { required this.spendsUnconfirmedTX, }); } + diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index b39821dbb..a0424c934 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,5 +1,4 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -30,7 +29,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { Map? initialChangeAddressIndex, List? initialSilentAddresses, int initialSilentAddressIndex = 0, - bitcoin.HDWallet? masterHd, + Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = @@ -53,9 +52,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { super(walletInfo) { if (masterHd != null) { silentAddress = SilentPaymentOwner.fromPrivateKeys( - b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privKey!), - b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privKey!), - hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp'); + b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privateKey.toHex()), + b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privateKey.toHex()), + network: network, + ); if (silentAddresses.length == 0) { silentAddresses.add(BitcoinSilentPaymentAddressRecord( @@ -92,8 +92,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final ObservableList changeAddresses; final ObservableList silentAddresses; final BasedUtxoNetwork network; - final bitcoin.HDWallet mainHd; - final bitcoin.HDWallet sideHd; + final Bip32Slip10Secp256k1 mainHd; + final Bip32Slip10Secp256k1 sideHd; @observable SilentPaymentOwner? silentAddress; @@ -318,7 +318,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } String getAddress( - {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => + {required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType}) => ''; @override @@ -540,11 +542,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { void _validateAddresses() { _addresses.forEach((element) { - if (!element.isHidden && element.address != - getAddress(index: element.index, hd: mainHd, addressType: element.type)) { + if (!element.isHidden && + element.address != + getAddress(index: element.index, hd: mainHd, addressType: element.type)) { element.isHidden = true; - } else if (element.isHidden && element.address != - getAddress(index: element.index, hd: sideHd, addressType: element.type)) { + } else if (element.isHidden && + element.address != + getAddress(index: element.index, hd: sideHd, addressType: element.type)) { element.isHidden = false; } }); @@ -562,7 +566,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return _isAddressByType(addressRecord, addressPageType); } - bitcoin.HDWallet _getHd(bool isHidden) => isHidden ? sideHd : mainHd; + Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd; bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => !addr.isHidden && !addr.isUsed && addr.type == type; diff --git a/cw_bitcoin/lib/litecoin_network.dart b/cw_bitcoin/lib/litecoin_network.dart deleted file mode 100644 index d7ad2f837..000000000 --- a/cw_bitcoin/lib/litecoin_network.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:bitcoin_flutter/bitcoin_flutter.dart'; - -final litecoinNetwork = NetworkType( - messagePrefix: '\x19Litecoin Signed Message:\n', - bech32: 'ltc', - bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), - pubKeyHash: 0x30, - scriptHash: 0x32, - wif: 0xb0); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index bfb9a1b16..64e53ca5d 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,12 +1,12 @@ 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/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; -import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -38,7 +38,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - networkType: litecoinNetwork, + network: LitecoinNetwork.mainnet, initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: seedBytes, @@ -49,7 +49,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, - sideHd: accountHD.derive(1), + sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, ); autorun((_) { diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 99b7445fc..6945db445 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,5 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -22,6 +22,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @override String getAddress( - {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => + {required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType}) => generateP2WPKHAddress(hd: hd, index: index, network: network); } diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart index e3ebc00db..29d7a9bf3 100644 --- a/cw_bitcoin/lib/utils.dart +++ b/cw_bitcoin/lib/utils.dart @@ -1,68 +1,54 @@ -import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:flutter/foundation.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; -import 'package:hex/hex.dart'; - -bitcoin.PaymentData generatePaymentData({ - required bitcoin.HDWallet hd, - required int index, -}) { - final pubKey = hd.derive(index).pubKey!; - return PaymentData(pubkey: Uint8List.fromList(HEX.decode(pubKey))); -} +import 'package:blockchain_utils/blockchain_utils.dart'; ECPrivate generateECPrivate({ - required bitcoin.HDWallet hd, + required Bip32Slip10Secp256k1 hd, required BasedUtxoNetwork network, required int index, -}) { - final wif = hd.derive(index).wif!; - return ECPrivate.fromWif(wif, netVersion: network.wifNetVer); -} +}) => + ECPrivate(hd.childKey(Bip32KeyIndex(index)).privateKey); String generateP2WPKHAddress({ - required bitcoin.HDWallet hd, + required Bip32Slip10Secp256k1 hd, required BasedUtxoNetwork network, required int index, -}) { - final pubKey = hd.derive(index).pubKey!; - return ECPublic.fromHex(pubKey).toP2wpkhAddress().toAddress(network); -} +}) => + ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) + .toP2wpkhAddress() + .toAddress(network); String generateP2SHAddress({ - required bitcoin.HDWallet hd, + required Bip32Slip10Secp256k1 hd, required BasedUtxoNetwork network, required int index, -}) { - final pubKey = hd.derive(index).pubKey!; - return ECPublic.fromHex(pubKey).toP2wpkhInP2sh().toAddress(network); -} +}) => + ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) + .toP2wshInP2sh() + .toAddress(network); String generateP2WSHAddress({ - required bitcoin.HDWallet hd, + required Bip32Slip10Secp256k1 hd, required BasedUtxoNetwork network, required int index, -}) { - final pubKey = hd.derive(index).pubKey!; - return ECPublic.fromHex(pubKey).toP2wshAddress().toAddress(network); -} +}) => + ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) + .toP2wshAddress() + .toAddress(network); String generateP2PKHAddress({ - required bitcoin.HDWallet hd, + required Bip32Slip10Secp256k1 hd, required BasedUtxoNetwork network, required int index, -}) { - final pubKey = hd.derive(index).pubKey!; - return ECPublic.fromHex(pubKey).toP2pkhAddress().toAddress(network); -} +}) => + ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) + .toP2pkhAddress() + .toAddress(network); String generateP2TRAddress({ - required bitcoin.HDWallet hd, + required Bip32Slip10Secp256k1 hd, required BasedUtxoNetwork network, required int index, -}) { - final pubKey = hd.derive(index).pubKey!; - return ECPublic.fromHex(pubKey).toTaprootAddress().toAddress(network); -} +}) => + ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) + .toTaprootAddress() + .toAddress(network); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 15f7cdb43..be7862e26 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -41,15 +41,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" - bech32: - dependency: transitive - description: - path: "." - ref: "cake-0.2.2" - resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" - url: "https://github.com/cake-tech/bech32.git" - source: git - version: "0.2.2" bip32: dependency: transitive description: @@ -79,29 +70,20 @@ packages: dependency: "direct main" description: path: "." - ref: cake-update-v3 - resolved-ref: cc99eedb1d28ee9376dda0465ef72aa627ac6149 + ref: cake-update-v4 + resolved-ref: "574486bfcdbbaf978dcd006b46fc8716f880da29" url: "https://github.com/cake-tech/bitcoin_base" source: git - version: "4.2.1" - bitcoin_flutter: - dependency: "direct main" - description: - path: "." - ref: cake-update-v4 - resolved-ref: e19ffb7e7977278a75b27e0479b3c6f4034223b3 - url: "https://github.com/cake-tech/bitcoin_flutter.git" - source: git - version: "2.1.0" + version: "4.7.0" blockchain_utils: dependency: "direct main" description: path: "." - ref: cake-update-v1 - resolved-ref: cabd7e0e16c4da9920338c76eff3aeb8af0211f3 + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" url: "https://github.com/cake-tech/blockchain_utils" source: git - version: "2.1.2" + version: "3.3.0" boolean_selector: dependency: transitive description: @@ -411,10 +393,10 @@ packages: dependency: "direct main" description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -499,11 +481,12 @@ packages: ledger_flutter: dependency: "direct main" description: - name: ledger_flutter - sha256: f1680060ed6ff78f275837e0024ccaf667715a59ba7aa29fa7354bc7752e71c8 - url: "https://pub.dev" - source: hosted - version: "1.0.1" + path: "." + ref: cake-v3 + resolved-ref: "66469ff9dffe2417c70ae7287c9d76d2fe7157a4" + url: "https://github.com/cake-tech/ledger-flutter.git" + source: git + version: "1.0.2" ledger_usb: dependency: transitive description: @@ -596,10 +579,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: @@ -636,18 +619,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -700,10 +683,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" quiver: dependency: transitive description: @@ -761,10 +744,10 @@ packages: dependency: transitive description: name: socks5_proxy - sha256: "045cbba84f6e2b01c1c77634a63e926352bf110ef5f07fc462c6d43bbd4b6a83" + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" url: "https://pub.dev" source: hosted - version: "1.0.5+dev.2" + version: "1.0.6" source_gen: dependency: transitive description: @@ -793,9 +776,9 @@ packages: dependency: "direct main" description: path: "." - ref: "sp_v2.0.0" - resolved-ref: "62c152b9086cd968019128845371072f7e1168de" - url: "https://github.com/cake-tech/sp_scanner" + ref: "sp_v4.0.0" + resolved-ref: "3b8ae38592c0584f53560071dc18bc570758fe13" + url: "https://github.com/rafael-xmr/sp_scanner" source: git version: "0.0.1" stack_trace: @@ -910,14 +893,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.5" - win32: - dependency: transitive - description: - name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" - url: "https://pub.dev" - source: hosted - version: "5.5.0" xdg_directories: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 69ff3d29b..449833220 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -19,10 +19,6 @@ dependencies: intl: ^0.18.0 cw_core: path: ../cw_core - bitcoin_flutter: - git: - url: https://github.com/cake-tech/bitcoin_flutter.git - ref: cake-update-v4 bitbox: git: url: https://github.com/cake-tech/bitbox-flutter.git @@ -32,19 +28,19 @@ dependencies: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v3 + ref: cake-update-v4 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v1 + ref: cake-update-v2 ledger_flutter: ^1.0.1 ledger_bitcoin: git: url: https://github.com/cake-tech/ledger-bitcoin sp_scanner: git: - url: https://github.com/cake-tech/sp_scanner - ref: sp_v2.0.0 + url: https://github.com/rafael-xmr/sp_scanner + ref: sp_v4.0.0 dev_dependencies: diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index f15eed10d..8323c01a8 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -1,8 +1,6 @@ -import 'dart:convert'; - import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -40,7 +38,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - networkType: bitcoin.bitcoin, + network: BitcoinCashNetwork.mainnet, initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: seedBytes, @@ -51,7 +49,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, - sideHd: accountHD.derive(1), + sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, initialAddressPageType: addressPageType, ); @@ -77,7 +75,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, - seedBytes: await Mnemonic.toSeed(mnemonic), + seedBytes: await MnemonicBip39.toSeed(mnemonic), initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, @@ -136,15 +134,17 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { } }).toList(), initialBalance: snp?.balance, - seedBytes: await Mnemonic.toSeed(keysData.mnemonic!), + seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!), initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, ); } - bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) => - bitbox.ECPair.fromWIF(hd.derive(index).wif!); + bitbox.ECPair generateKeyPair({required Bip32Slip10Secp256k1 hd, required int index}) => + bitbox.ECPair.fromPrivateKey( + Uint8List.fromList(hd.childKey(Bip32KeyIndex(index)).privateKey.raw), + ); int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) { int inputsCount = 0; @@ -190,7 +190,11 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { .firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address)) .index : null; - final HD = index == null ? hd : hd.derive(index); - return base64Encode(HD.signMessage(message)); + final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index)); + final priv = ECPrivate.fromWif( + WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer), + netVersion: network.wifNetVer, + ); + return priv.signMessage(StringUtils.encode(message)); } } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index d543e944c..7342dc7f5 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -1,5 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/wallet_info.dart'; @@ -23,6 +23,8 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi @override String getAddress( - {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => + {required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType}) => generateP2PKHAddress(hd: hd, index: index, network: network); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index 01ae8ace3..002e52c4f 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -11,8 +11,11 @@ import 'package:cw_core/wallet_type.dart'; import 'package:collection/collection.dart'; import 'package:hive/hive.dart'; -class BitcoinCashWalletService extends WalletService { +class BitcoinCashWalletService extends WalletService< + BitcoinCashNewWalletCredentials, + BitcoinCashRestoreWalletFromSeedCredentials, + BitcoinCashRestoreWalletFromWIFCredentials, + BitcoinCashNewWalletCredentials> { BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -30,7 +33,7 @@ class BitcoinCashWalletService extends WalletService restoreFromHardwareWallet(BitcoinCashNewWalletCredentials credentials) { - throw UnimplementedError("Restoring a Bitcoin Cash wallet from a hardware wallet is not yet supported!"); + throw UnimplementedError( + "Restoring a Bitcoin Cash wallet from a hardware wallet is not yet supported!"); } @override diff --git a/cw_bitcoin_cash/lib/src/mnemonic.dart b/cw_bitcoin_cash/lib/src/mnemonic.dart index b1f1ee984..7aac1d5c4 100644 --- a/cw_bitcoin_cash/lib/src/mnemonic.dart +++ b/cw_bitcoin_cash/lib/src/mnemonic.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; import 'package:bip39/bip39.dart' as bip39; -class Mnemonic { +class MnemonicBip39 { /// Generate bip39 mnemonic static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength); diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index a0ce889c1..3728bafc5 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -21,10 +21,6 @@ dependencies: path: ../cw_core cw_bitcoin: path: ../cw_bitcoin - bitcoin_flutter: - git: - url: https://github.com/cake-tech/bitcoin_flutter.git - ref: cake-update-v4 bitbox: git: url: https://github.com/cake-tech/bitbox-flutter.git @@ -32,11 +28,11 @@ dependencies: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v3 + ref: cake-update-v4 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v1 + ref: cake-update-v2 dev_dependencies: flutter_test: diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 6f1b4078b..204f03d62 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -245,6 +245,8 @@ Future getHavenCurrentHeight() async { // Data taken from https://timechaincalendar.com/ const bitcoinDates = { + "2024-08": 854889, + "2024-07": 850182, "2024-06": 846005, "2024-05": 841590, "2024-04": 837182, @@ -371,7 +373,8 @@ const wowDates = { int getWowneroHeightByDate({required DateTime date}) { String closestKey = - wowDates.keys.firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => ''); + wowDates.keys.firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => ''); return wowDates[closestKey] ?? 0; -} \ No newline at end of file +} + diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index f35ea589f..85c61de15 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -11,7 +11,8 @@ import 'package:http/io_client.dart' as ioc; part 'node.g.dart'; -Uri createUriFromElectrumAddress(String address, String path) => Uri.tryParse('tcp://$address$path')!; +Uri createUriFromElectrumAddress(String address, String path) => + Uri.tryParse('tcp://$address$path')!; @HiveType(typeId: Node.typeId) class Node extends HiveObject with Keyable { @@ -72,6 +73,12 @@ class Node extends HiveObject with Keyable { @HiveField(7, defaultValue: '') String? path; + @HiveField(8) + bool? isElectrs; + + @HiveField(9) + bool? supportsSilentPayments; + bool get isSSL => useSSL ?? false; bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 55c31877f..ea015340c 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -3,6 +3,11 @@ abstract class SyncStatus { double progress(); } +class StartingScanSyncStatus extends SyncStatus { + @override + double progress() => 0.0; +} + class SyncingSyncStatus extends SyncStatus { SyncingSyncStatus(this.blocksLeft, this.ptc); diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index e363d88db..971e4ecdb 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -9,7 +9,7 @@ abstract class TransactionInfo extends Object with Keyable { late TransactionDirection direction; late bool isPending; late DateTime date; - late int height; + int? height; late int confirmations; String amountFormatted(); String fiatAmount(); @@ -25,4 +25,5 @@ abstract class TransactionInfo extends Object with Keyable { dynamic get keyIndex => id; late Map additionalInfo; -} \ No newline at end of file +} + diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index b8583d219..c34e164f4 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -254,10 +254,10 @@ packages: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: @@ -514,14 +514,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" pub_semver: dependency: transitive description: @@ -707,10 +699,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" yaml: dependency: transitive description: diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 838f7224c..38299b2dc 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -438,8 +438,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "bcb328a4956105dc182afd0ce2e48fe263f5f20b" - resolved-ref: "bcb328a4956105dc182afd0ce2e48fe263f5f20b" + ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b + resolved-ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index 70f2f6f0b..c3d4b26b6 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: b74e3842a52c61f8819a1ec8444b4de5419b41a7465e69d4aa681445377398b0 + sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.5.3" async: dependency: transitive description: @@ -114,7 +114,7 @@ packages: source: hosted version: "2.4.9" build_runner_core: - dependency: transitive + dependency: "direct overridden" description: name: build_runner_core sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.9.2" characters: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.10.0" collection: dependency: transitive description: @@ -220,10 +220,10 @@ packages: dependency: "direct main" description: name: ed25519_hd_key - sha256: "326608234e986ea826a5db4cf4cd6826058d860875a3fff7926c0725fe1a604d" + sha256: c5c9f11a03f5789bf9dcd9ae88d641571c802640851f1cacdb13123f171b3a26 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" encrypt: dependency: transitive description: @@ -244,10 +244,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" file: dependency: transitive description: @@ -475,10 +475,10 @@ packages: dependency: "direct main" description: name: mobx - sha256: "0afcf88b3ee9d6819890bf16c11a727fc8c62cf736fda8e5d3b9b4eace4e62ea" + sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.3+2" mobx_codegen: dependency: "direct dev" description: @@ -572,10 +572,10 @@ packages: dependency: transitive description: name: pinenacl - sha256: e5fb0bce1717b7f136f35ee98b5c02b3e6383211f8a77ca882fa7812232a07b9 + sha256: "3a5503637587d635647c93ea9a8fecf48a420cc7deebe6f1fc85c2a5637ab327" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.5.1" platform: dependency: transitive description: @@ -588,10 +588,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.8" pointycastle: dependency: transitive description: @@ -652,10 +652,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.5.0" shared_preferences_linux: dependency: transitive description: @@ -668,10 +668,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_web: dependency: transitive description: @@ -849,18 +849,18 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.5.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" yaml: dependency: transitive description: @@ -870,5 +870,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.16.6" diff --git a/cw_nano/pubspec.yaml b/cw_nano/pubspec.yaml index 768c1bb4e..6fae6a895 100644 --- a/cw_nano/pubspec.yaml +++ b/cw_nano/pubspec.yaml @@ -38,6 +38,7 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 + build_runner_core: 7.2.7+1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index f27e1b460..e69fd7ca0 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -18,11 +18,11 @@ dependencies: on_chain: git: url: https://github.com/cake-tech/On_chain - ref: cake-update-v1 + ref: cake-update-v2 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v1 + ref: cake-update-v2 mobx: ^2.3.0+1 bip39: ^1.0.6 hive: ^2.2.3 diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index d91922ac9..737743925 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -254,10 +254,10 @@ packages: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: @@ -438,8 +438,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "bcb328a4956105dc182afd0ce2e48fe263f5f20b" - resolved-ref: "bcb328a4956105dc182afd0ce2e48fe263f5f20b" + ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b + resolved-ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -555,14 +555,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" pub_semver: dependency: transitive description: @@ -748,10 +740,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" yaml: dependency: transitive description: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fddf6e24f..fb6dc4ecf 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -94,6 +94,8 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - sp_scanner (0.0.1): + - Flutter - SwiftProtobuf (1.26.0) - SwiftyGif (5.4.5) - Toast (4.1.1) @@ -132,6 +134,7 @@ DEPENDENCIES: - 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`) + - sp_scanner (from `.symlinks/plugins/sp_scanner/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -197,6 +200,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sp_scanner: + :path: ".symlinks/plugins/sp_scanner/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: @@ -227,7 +232,7 @@ SPEC CHECKSUMS: MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 Protobuf: fb2c13674723f76ff6eede14f78847a776455fa2 @@ -235,15 +240,16 @@ SPEC CHECKSUMS: reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 - share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 SwiftProtobuf: 5e8349171e7c2f88f5b9e683cb3cb79d1dc780b3 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 + wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 PODFILE CHECKSUM: a2fe518be61cdbdc5b0e2da085ab543d556af2d3 diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index a92aaad74..edfc77acb 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -302,16 +302,13 @@ class CWBitcoin extends Bitcoin { await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); late BasedUtxoNetwork network; - btc.NetworkType networkType; switch (node.type) { case WalletType.litecoin: network = LitecoinNetwork.mainnet; - networkType = litecoinNetwork; break; case WalletType.bitcoin: default: network = BitcoinNetwork.mainnet; - networkType = btc.bitcoin; break; } @@ -341,10 +338,8 @@ class CWBitcoin extends Bitcoin { balancePath += "/0"; } - final hd = btc.HDWallet.fromSeed( - seedBytes, - network: networkType, - ).derivePath(balancePath); + final hd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(balancePath) + as Bip32Slip10Secp256k1; // derive address at index 0: String? address; @@ -515,10 +510,7 @@ class CWBitcoin extends Bitcoin { @override Future setScanningActive(Object wallet, bool active) async { final bitcoinWallet = wallet as ElectrumWallet; - bitcoinWallet.setSilentPaymentsScanning( - active, - active && (await getNodeIsElectrsSPEnabled(wallet)), - ); + bitcoinWallet.setSilentPaymentsScanning(active); } @override @@ -536,44 +528,10 @@ class CWBitcoin extends Bitcoin { bitcoinWallet.rescan(height: height, doSingleScan: doSingleScan); } - Future getNodeIsElectrs(Object wallet) async { - final bitcoinWallet = wallet as ElectrumWallet; - - final version = await bitcoinWallet.electrumClient.version(); - - if (version.isEmpty) { - return false; - } - - final server = version[0]; - - if (server.toLowerCase().contains('electrs')) { - return true; - } - - return false; - } - @override Future getNodeIsElectrsSPEnabled(Object wallet) async { - if (!(await getNodeIsElectrs(wallet))) { - return false; - } - final bitcoinWallet = wallet as ElectrumWallet; - try { - final tweaksResponse = await bitcoinWallet.electrumClient.getTweaks(height: 0); - - if (tweaksResponse != null) { - return true; - } - } on RequestFailedTimeoutException catch (_) { - return false; - } catch (_) { - rethrow; - } - - return false; + return bitcoinWallet.getNodeSupportsSilentPayments(); } @override diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index c4cc3929f..465211f23 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -52,5 +52,9 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_syncronizing; } + if (syncStatus is StartingScanSyncStatus) { + return S.current.sync_status_starting_scan; + } + return ''; } diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart index fd8dce103..02ddf037d 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -34,7 +34,7 @@ class CakePayBuyCardDetailPage extends BasePage { @override Widget? middle(BuildContext context) { - return Text( + return Text( title, textAlign: TextAlign.center, maxLines: 2, @@ -359,7 +359,7 @@ class CakePayBuyCardDetailPage extends BasePage { reaction((_) => cakePayPurchaseViewModel.sendViewModel.state, (ExecutionState state) { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { - showStateAlert(context, S.of(context).error, state.error); + if (context.mounted) showStateAlert(context, S.of(context).error, state.error); }); } @@ -381,31 +381,35 @@ class CakePayBuyCardDetailPage extends BasePage { } void showStateAlert(BuildContext context, String title, String content) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: title, - alertContent: content, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); + if (context.mounted) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: title, + alertContent: content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } } Future showSentAlert(BuildContext context) async { + if (!context.mounted) { + return; + } final order = cakePayPurchaseViewModel.order!.orderId; final isCopy = await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).transaction_sent, - alertContent: - S.of(context).cake_pay_save_order + '\n${order}', - leftButtonText: S.of(context).ignor, - rightButtonText: S.of(context).copy, - actionLeftButton: () => Navigator.of(context).pop(false), - actionRightButton: () => Navigator.of(context).pop(true)); - }) ?? + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).transaction_sent, + alertContent: S.of(context).cake_pay_save_order + '\n${order}', + leftButtonText: S.of(context).ignor, + rightButtonText: S.of(context).copy, + actionLeftButton: () => Navigator.of(context).pop(false), + actionRightButton: () => Navigator.of(context).pop(true)); + }) ?? false; if (isCopy) { diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 463c04988..90072a7c1 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -87,10 +87,6 @@ dependencies: git: url: https://github.com/cake-tech/ens_dart.git ref: main - bitcoin_flutter: - git: - url: https://github.com/cake-tech/bitcoin_flutter.git - ref: cake-update-v4 fluttertoast: 8.1.4 # tor: # git: @@ -104,7 +100,7 @@ dependencies: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v3 + ref: cake-update-v4 ledger_flutter: ^1.0.1 hashlib: 1.12.0 diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index d543706fc..4cf9509f8 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "يتم التوصيل", "sync_status_failed_connect": "انقطع الاتصال", "sync_status_not_connected": "غير متصل", + "sync_status_starting_scan": "بدء المسح", "sync_status_starting_sync": "بدء المزامنة", "sync_status_syncronized": "متزامن", "sync_status_syncronizing": "يتم المزامنة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index ede60567d..b76401cf4 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "СВЪРЗВАНЕ", "sync_status_failed_connect": "НЕУСПЕШНО СВЪРЗВАНЕ", "sync_status_not_connected": "НЯМА ВРЪЗКА", + "sync_status_starting_scan": "Стартово сканиране", "sync_status_starting_sync": "ЗАПОЧВАНЕ НА СИНХРОНИЗАЦИЯ", "sync_status_syncronized": "СИНХРОНИЗИРАНО", "sync_status_syncronizing": "СИНХРОНИЗИРАНЕ", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 8f2cda1e0..c5d374dd0 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "PŘIPOJOVÁNÍ", "sync_status_failed_connect": "ODPOJENO", "sync_status_not_connected": "NEPŘIPOJENO", + "sync_status_starting_scan": "Počáteční skenování", "sync_status_starting_sync": "SPOUŠTĚNÍ SYNCHRONIZACE", "sync_status_syncronized": "SYNCHRONIZOVÁNO", "sync_status_syncronizing": "SYNCHRONIZUJI", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 59a23222c..7b6613dd6 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -696,6 +696,7 @@ "sync_status_connecting": "VERBINDEN", "sync_status_failed_connect": "GETRENNT", "sync_status_not_connected": "NICHT VERBUNDEN", + "sync_status_starting_scan": "Scan beginnen", "sync_status_starting_sync": "STARTE SYNCHRONISIERUNG", "sync_status_syncronized": "SYNCHRONISIERT", "sync_status_syncronizing": "SYNCHRONISIERE", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1bd3fc241..35712a780 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -696,6 +696,7 @@ "sync_status_connecting": "CONNECTING", "sync_status_failed_connect": "DISCONNECTED", "sync_status_not_connected": "NOT CONNECTED", + "sync_status_starting_scan": "STARTING SCAN", "sync_status_starting_sync": "STARTING SYNC", "sync_status_syncronized": "SYNCHRONIZED", "sync_status_syncronizing": "SYNCHRONIZING", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index dc8aa3b95..31a6e6865 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -696,6 +696,7 @@ "sync_status_connecting": "CONECTANDO", "sync_status_failed_connect": "DESCONECTADO", "sync_status_not_connected": "NO CONECTADO", + "sync_status_starting_scan": "Escaneo inicial", "sync_status_starting_sync": "EMPEZANDO A SINCRONIZAR", "sync_status_syncronized": "SINCRONIZADO", "sync_status_syncronizing": "SINCRONIZANDO", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index f7c45f7ef..03d4a73dd 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "CONNEXION EN COURS", "sync_status_failed_connect": "DÉCONNECTÉ", "sync_status_not_connected": "NON CONNECTÉ", + "sync_status_starting_scan": "Démarrage", "sync_status_starting_sync": "DÉBUT DE SYNCHRO", "sync_status_syncronized": "SYNCHRONISÉ", "sync_status_syncronizing": "SYNCHRONISATION EN COURS", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index a5805bbb8..922f9a51b 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -697,6 +697,7 @@ "sync_status_connecting": "HADA", "sync_status_failed_connect": "BABU INTERNET", "sync_status_not_connected": "BABU INTERNET", + "sync_status_starting_scan": "Fara scan", "sync_status_starting_sync": "KWAFI", "sync_status_syncronized": "KYAU", "sync_status_syncronizing": "KWAFI", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 4ab8e7534..db6940e8b 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -697,6 +697,7 @@ "sync_status_connecting": "कनेक्ट", "sync_status_failed_connect": "डिस्कनेक्ट किया गया", "sync_status_not_connected": "जुड़े नहीं हैं", + "sync_status_starting_scan": "स्कैन शुरू करना", "sync_status_starting_sync": "सिताज़ा करना", "sync_status_syncronized": "सिंक्रनाइज़", "sync_status_syncronizing": "सिंक्रनाइज़ करने", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 67095ba8f..57cf1361e 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "SPAJANJE", "sync_status_failed_connect": "ISKLJUČENO", "sync_status_not_connected": "NIJE POVEZANO", + "sync_status_starting_scan": "Početno skeniranje", "sync_status_starting_sync": "ZAPOČINJEMO SINKRONIZIRANJE", "sync_status_syncronized": "SINKRONIZIRANO", "sync_status_syncronizing": "SINKRONIZIRANJE", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 939b938fe..97a4afd3f 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -698,6 +698,7 @@ "sync_status_connecting": "MENGHUBUNGKAN", "sync_status_failed_connect": "GAGAL TERHUBUNG", "sync_status_not_connected": "TIDAK TERHUBUNG", + "sync_status_starting_scan": "Mulai pindai", "sync_status_starting_sync": "MULAI SINKRONISASI", "sync_status_syncronized": "SUDAH TERSINKRONISASI", "sync_status_syncronizing": "SEDANG SINKRONISASI", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 29a142d1e..42c2e628d 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -697,6 +697,7 @@ "sync_status_connecting": "CONNESSIONE", "sync_status_failed_connect": "DISCONNESSO", "sync_status_not_connected": "NON CONNESSO", + "sync_status_starting_scan": "Scansione di partenza", "sync_status_starting_sync": "INIZIO SINC", "sync_status_syncronized": "SINCRONIZZATO", "sync_status_syncronizing": "SINCRONIZZAZIONE", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 3009aa115..72b1f7d09 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -696,6 +696,7 @@ "sync_status_connecting": "接続中", "sync_status_failed_connect": "切断されました", "sync_status_not_connected": "接続されていません", + "sync_status_starting_scan": "スキャンを開始します", "sync_status_starting_sync": "同期の開始", "sync_status_syncronized": "同期された", "sync_status_syncronizing": "同期", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 53b3cc875..b8cfee1b5 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -696,6 +696,7 @@ "sync_status_connecting": "연결 중", "sync_status_failed_connect": "연결 해제", "sync_status_not_connected": "연결되지 않은", + "sync_status_starting_scan": "스캔 시작", "sync_status_starting_sync": "동기화 시작", "sync_status_syncronized": "동기화", "sync_status_syncronizing": "동기화", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 64a7a1ad1..52fe72ea6 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "ချိတ်ဆက်ခြင်း။", "sync_status_failed_connect": "အဆက်အသွယ်ဖြတ်ထားသည်။", "sync_status_not_connected": "မချိတ်ဆက်ပါ။", + "sync_status_starting_scan": "စကင်ဖတ်စစ်ဆေးမှု", "sync_status_starting_sync": "စင့်ခ်လုပ်ခြင်း။", "sync_status_syncronized": "ထပ်တူပြုထားသည်။", "sync_status_syncronizing": "ထပ်တူပြုခြင်း။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 86f6b8c0b..cde10506f 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "AANSLUITING", "sync_status_failed_connect": "LOSGEKOPPELD", "sync_status_not_connected": "NIET VERBONDEN", + "sync_status_starting_scan": "Startscan", "sync_status_starting_sync": "BEGINNEN MET SYNCHRONISEREN", "sync_status_syncronized": "SYNCHRONIZED", "sync_status_syncronizing": "SYNCHRONISEREN", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 34a8d57fe..a22034c96 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "ŁĄCZENIE", "sync_status_failed_connect": "POŁĄCZENIE NIEUDANE", "sync_status_not_connected": "NIE POŁĄCZONY", + "sync_status_starting_scan": "Rozpoczęcie skanowania", "sync_status_starting_sync": "ROZPOCZĘCIE SYNCHRONIZACJI", "sync_status_syncronized": "ZSYNCHRONIZOWANO", "sync_status_syncronizing": "SYNCHRONIZACJA", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 67d68988f..8f87ca59f 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -697,6 +697,7 @@ "sync_status_connecting": "CONECTANDO", "sync_status_failed_connect": "DESCONECTADO", "sync_status_not_connected": "DESCONECTADO", + "sync_status_starting_scan": "Diretor inicial", "sync_status_starting_sync": "INICIANDO SINCRONIZAÇÃO", "sync_status_syncronized": "SINCRONIZADO", "sync_status_syncronizing": "SINCRONIZANDO", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 521cda83d..9f360137f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -696,6 +696,7 @@ "sync_status_connecting": "ПОДКЛЮЧЕНИЕ", "sync_status_failed_connect": "ОТКЛЮЧЕНО", "sync_status_not_connected": "НЕ ПОДКЛЮЧЁН", + "sync_status_starting_scan": "Начальное сканирование", "sync_status_starting_sync": "НАЧАЛО СИНХРОНИЗАЦИИ", "sync_status_syncronized": "СИНХРОНИЗИРОВАН", "sync_status_syncronizing": "СИНХРОНИЗАЦИЯ", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 996472f47..a178d2452 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "กำลังเชื่อมต่อ", "sync_status_failed_connect": "การเชื่อมต่อล้มเหลว", "sync_status_not_connected": "ไม่ได้เชื่อมต่อ", + "sync_status_starting_scan": "เริ่มการสแกน", "sync_status_starting_sync": "กำลังเริ่มซิงโครไนซ์", "sync_status_syncronized": "ซิงโครไนซ์แล้ว", "sync_status_syncronizing": "กำลังซิงโครไนซ์", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 27e4974bb..f49d3ddee 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "Pagkonekta", "sync_status_failed_connect": "Naka -disconnect", "sync_status_not_connected": "HINDI KONEKTADO", + "sync_status_starting_scan": "Simula sa pag -scan", "sync_status_starting_sync": "Simula sa pag -sync", "sync_status_syncronized": "Naka -synchronize", "sync_status_syncronizing": "Pag -synchronize", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 74b72581e..c73765f64 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "BAĞLANILIYOR", "sync_status_failed_connect": "BAĞLANTI KESİLDİ", "sync_status_not_connected": "BAĞLI DEĞİL", + "sync_status_starting_scan": "Başlangıç ​​taraması", "sync_status_starting_sync": "SENKRONİZE BAŞLATILIYOR", "sync_status_syncronized": "SENKRONİZE EDİLDİ", "sync_status_syncronizing": "SENKRONİZE EDİLİYOR", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 74b2e4703..d088dd1b2 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -696,6 +696,7 @@ "sync_status_connecting": "ПІДКЛЮЧЕННЯ", "sync_status_failed_connect": "ВІДКЛЮЧЕНО", "sync_status_not_connected": "НЕ ПІДКЛЮЧЕННИЙ", + "sync_status_starting_scan": "Початок сканування", "sync_status_starting_sync": "ПОЧАТОК СИНХРОНІЗАЦІЇ", "sync_status_syncronized": "СИНХРОНІЗОВАНИЙ", "sync_status_syncronizing": "СИНХРОНІЗАЦІЯ", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 35d024188..0694463de 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -697,6 +697,7 @@ "sync_status_connecting": "جڑ رہا ہے۔", "sync_status_failed_connect": "منقطع", "sync_status_not_connected": "منسلک نہیں", + "sync_status_starting_scan": "اسکین شروع کرنا", "sync_status_starting_sync": "مطابقت پذیری شروع کر رہا ہے۔", "sync_status_syncronized": "مطابقت پذیر", "sync_status_syncronizing": "مطابقت پذیری", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 29b8d9b71..87df87aca 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -696,6 +696,7 @@ "sync_status_connecting": "Ń DÁRAPỌ̀ MỌ́", "sync_status_failed_connect": "ÌKÀNPỌ̀ TI KÚ", "sync_status_not_connected": "KÒ TI DÁRAPỌ̀ MỌ́ Ọ", + "sync_status_starting_scan": "Bibẹrẹ ọlọjẹ", "sync_status_starting_sync": "Ń BẸ̀RẸ̀ RẸ́", "sync_status_syncronized": "TI MÚDỌ́GBA", "sync_status_syncronizing": "Ń MÚDỌ́GBA", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index a30acad70..89eca2073 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -695,6 +695,7 @@ "sync_status_connecting": "连接中", "sync_status_failed_connect": "断线", "sync_status_not_connected": "未连接", + "sync_status_starting_scan": "开始扫描", "sync_status_starting_sync": "开始同步", "sync_status_syncronized": "已同步", "sync_status_syncronizing": "正在同步", diff --git a/tool/configure.dart b/tool/configure.dart index 8b5af92b2..c37946476 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -94,12 +94,11 @@ import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter/ledger_flutter.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as btc; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bip39/bip39.dart' as bip39; """; const bitcoinCWHeaders = """ import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; From 88a57c1541e06264a3f690c4d83bb8918e29621d Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 12 Aug 2024 02:53:13 +0300 Subject: [PATCH 019/142] V4.19.2 v1.16.2 (#1591) * refactor: remove bitcoin_flutter, update deps, electrs node improvements * feat: connecting/disconnecting improvements, fix rescan by date, scanning message * chore: print * Update pubspec.yaml * Update pubspec.yaml * handle null sockets, retry connection on connect failure * fix imports * update app versions * fix transaction history * fix RBF * update android build number [skip ci] --------- Co-authored-by: Rafael Saes Co-authored-by: Matthew Fosse --- assets/text/Monerocom_Release_Notes.txt | 5 +++-- assets/text/Release_Notes.txt | 9 +++++---- scripts/android/app_env.sh | 8 ++++---- scripts/ios/app_env.sh | 8 ++++---- scripts/macos/app_env.sh | 8 ++++---- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 2a6c07abe..c90d54524 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,4 @@ -Monero enhancements -Synchronization improvements +Monero synchronization improvements +Enhance error handling +UI enhancements Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index d17a22c84..34bca2e5e 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,5 +1,6 @@ -Monero and Ethereum enhancements -Synchronization improvements -Exchange flow enhancements -Ledger improvements +Wallets enhancements +Monero synchronization improvements +Improve wallet backups +Enhance error handling +UI enhancements Bug fixes \ No newline at end of file diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index a270afab0..35444dcd5 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.1" -MONERO_COM_BUILD_NUMBER=95 +MONERO_COM_VERSION="1.16.2" +MONERO_COM_BUILD_NUMBER=96 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.1" -CAKEWALLET_BUILD_NUMBER=221 +CAKEWALLET_VERSION="4.19.2" +CAKEWALLET_BUILD_NUMBER=223 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 22daba5de..30573035a 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.1" -MONERO_COM_BUILD_NUMBER=93 +MONERO_COM_VERSION="1.16.2" +MONERO_COM_BUILD_NUMBER=94 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.1" -CAKEWALLET_BUILD_NUMBER=256 +CAKEWALLET_VERSION="4.19.2" +CAKEWALLET_BUILD_NUMBER=261 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index d46900405..2f6d51a93 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.6.1" -MONERO_COM_BUILD_NUMBER=26 +MONERO_COM_VERSION="1.6.2" +MONERO_COM_BUILD_NUMBER=27 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.12.1" -CAKEWALLET_BUILD_NUMBER=82 +CAKEWALLET_VERSION="1.12.2" +CAKEWALLET_BUILD_NUMBER=83 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 1809f91f0122fd4e3726da2edf2aecb2d6d7244c Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Mon, 12 Aug 2024 15:17:01 -0400 Subject: [PATCH 020/142] Fix logo sizing (#1592) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7823734fb..6e507bfcd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- +![logo](.github/assets/Logo_CakeWallet.png)
From 3b635218a5bee77e9f267b1504e78cd8d786b311 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 13 Aug 2024 01:04:05 +0300 Subject: [PATCH 021/142] minor fix (#1597) --- ios/Podfile.lock | 38 +++++++++++++++++++++++++ ios/Runner.xcodeproj/project.pbxproj | 6 ++-- lib/utils/exception_handler.dart | 1 + scripts/android/app_env.sh | 8 +++--- scripts/ios/app_env.sh | 8 +++--- scripts/windows/build_exe_installer.iss | 2 +- 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fb6dc4ecf..212d1ec1c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -8,6 +8,36 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.2) + - cw_haven (0.0.1): + - cw_haven/Boost (= 0.0.1) + - cw_haven/Haven (= 0.0.1) + - cw_haven/OpenSSL (= 0.0.1) + - cw_haven/Sodium (= 0.0.1) + - cw_shared_external + - Flutter + - cw_haven/Boost (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Haven (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/OpenSSL (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Sodium (0.0.1): + - cw_shared_external + - Flutter + - cw_shared_external (0.0.1): + - cw_shared_external/Boost (= 0.0.1) + - cw_shared_external/OpenSSL (= 0.0.1) + - cw_shared_external/Sodium (= 0.0.1) + - Flutter + - cw_shared_external/Boost (0.0.1): + - Flutter + - cw_shared_external/OpenSSL (0.0.1): + - Flutter + - cw_shared_external/Sodium (0.0.1): + - Flutter - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -115,6 +145,8 @@ DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift + - cw_haven (from `.symlinks/plugins/cw_haven/ios`) + - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -162,6 +194,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/barcode_scan2/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cw_haven: + :path: ".symlinks/plugins/cw_haven/ios" + cw_shared_external: + :path: ".symlinks/plugins/cw_shared_external/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -216,6 +252,8 @@ SPEC CHECKSUMS: BigInt: f668a80089607f521586bbe29513d708491ef2f7 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea + cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a + cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: b22617f40038496deffba44747101255cee005b0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 417c522a6..688fa2c39 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -483,7 +483,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 1.0.1; - PRODUCT_BUNDLE_IDENTIFIER = "com.fotolockr.cakewallet"; + PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -629,7 +629,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 1.0.1; - PRODUCT_BUNDLE_IDENTIFIER = "com.fotolockr.cakewallet"; + PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -667,7 +667,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 1.0.1; - PRODUCT_BUNDLE_IDENTIFIER = "com.fotolockr.cakewallet"; + PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 6045c0004..5e0c83f88 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -173,6 +173,7 @@ class ExceptionHandler { "OS Error: Network is unreachable", "ClientException: Write failed, uri=http", "Connection terminated during handshake", + "Corrupted wallets seeds", ]; static Future _addDeviceInfo(File file) async { diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 35444dcd5..c91f24622 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.2" -MONERO_COM_BUILD_NUMBER=96 +MONERO_COM_VERSION="1.16.3" +MONERO_COM_BUILD_NUMBER=97 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.2" -CAKEWALLET_BUILD_NUMBER=223 +CAKEWALLET_VERSION="4.19.3" +CAKEWALLET_BUILD_NUMBER=224 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 30573035a..e32b3e9f3 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.2" -MONERO_COM_BUILD_NUMBER=94 +MONERO_COM_VERSION="1.16.3" +MONERO_COM_BUILD_NUMBER=95 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.2" -CAKEWALLET_BUILD_NUMBER=261 +CAKEWALLET_VERSION="4.19.3" +CAKEWALLET_BUILD_NUMBER=262 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index ef26941d7..216f367ca 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.0.3" +#define MyAppVersion "0.0.4" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" From 1ce60d62b3121cfbd6b882c041283556c2c7bca7 Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 13 Aug 2024 00:18:14 +0200 Subject: [PATCH 022/142] CW-676 Add Linux scripts to build monero_c for linux platform (#1527) * Revert "Revert btc address types" This reverts commit a49e57e3 * Re-add Bitcoin Address types Fix conflicts with main * fix: label issues, clear spent utxo * chore: deps * fix: build * fix: missing types * feat: new electrs API & changes, fixes for last block scanning * Update Monero * not sure why it's failing * Enable Exolix Improve service updates indicator New versions * Add exolix Api token to limits api * Ignore reporting network issues * Change default bitcoin node * Merge main and update linux version * Update app version [skip ci] * New versions * Fix conflicts and update linux version * minor fix * feat: Scan Silent Payments homepage toggle * chore: build configure * feat: generic fixes, testnet UI improvements, useSSL on bitcoin nodes * fix: invalid Object in sendData * feat: improve addresses page & address book displays * feat: silent payments labeled addresses disclaimer * fix: missing i18n * chore: print * feat: single block scan, rescan by date working for btc mainnet * feat: new cake features page replace market page, move sp scan toggle, auto switch node pop up alert * feat: delete silent addresses * fix: red dot in non ssl nodes * fix: inconsistent connection states, fix tx history * fix: tx & balance displays, cpfp sending * feat: new rust lib * chore: node path * fix: check node based on network * fix: missing txcount from addresses * style: padding in feature page cards * fix: restore not getting all wallet addresses by type * fix: auto switch node broken * fix: silent payment txs not being restored * update linux version * feat: change scanning to subscription model, sync improvements * fix: scan re-subscription * fix: default nodes * fix: improve scanning by date, fix single block scan * refactor: common function for input tx selection * various fixes for build issues * initial monero.dart implementation * ... * multiple wallets new lib minor fixes * other fixes from monero.dart and monero_c * fix: nodes & build * update build scripts fix polyseed * remove unnecessary code * Add windows app, build scripts and build guide for it. * Minor fix in generated monero configs * Merge and fix main * fix: send all with multiple outs * add missing monero_c command * add android build script * update version * Merge and fix main * undo android ndk removal * Fix modified exception_handler.dart * Temporarily remove haven * fix build issues * fix pr script * Fixes for build monero.dart (monero_c) for windows. * monero build script * wip: ios build script * refactor: unchanged file * Added build guides for iOS and macOS. Replaced nproc call on macOS. Added macOS configuration for configure_cake_wallet.sh script. * Update monero.dart and monero_c versions. * Add missed windows build scripts * Update the application configuration for windows build script. * Update cw_monero pubspec lock file for monero.dart * Update pr_test_build.yml * chore: upgrade * chore: merge changes * refactor: unchanged files [skip ci] * Fix conflicts with main * fix for multiple wallets * update app version [skip ci] * Add tron to windows application configuration. * Add macOS option for description message in configure_cake_wallet.sh * fix missing encryption utils in hardware wallet functions [skip ci] * fix conflicts * Include missed monero dll for windows. * reformatting [skip ci] * fix conflicts with main * Disable haven configuration for iOS as default. Add ability to configure cakewallet for iOS with for configuration script. Remove cw_shared configuration for cw_monero. * fix: scan fixes, add date, allow sending while scanning * add missing nano secrets file [skip ci] * ios library * don't pull prebuilds android * Add auto generation of manifest file for android project even for iOS, macOS, Windows. * remove tron * feat: sync fixes, sp settings * feat: fix resyncing * store crash fix * make init async so it won't lag disable print starts * fix monero_c build issues * libstdc++ * merge main and update version * Fix MacOS saving wallet file issue Fix Secure Storage issue (somehow) * update pubspec.lock * fix build script * Use dylib as iOS framework. Use custom path for loading of iOS framework for monero.dart. Add script for generate iOS framework for monero wallet. * fix: date from height logic, status disconnected & chain tip get * fix: params * feat: electrum migration if using cake electrum * fix nodes update versions * re-enable tron * update sp_scanner to work on iOS [skip ci] * bump monero_c hash * bump monero_c commit * bump moneroc version * bump monero_c commit * Add ability to build monero wallet lib as universal lib. Update macOS build guide. Change default arch for macOS project to . * fix: wrong socket for old electrum nodes * update version * Fix unchecked wallet type call * get App Dir correctly in default_settings_migration.dart * handle previous issue with fetching linux documents directory [skip ci] * backup fix * fix NTFS issues * Add Tron Update Linux version * Close the wallet when the wallet gets changed * fix: double balance * feat: node domain * fix: menu name * bump monero_c commit * fix: update tip on set scanning * fix: connection switching back and forth * feat: check if node is electrs, and supports sp * chore: fix build * minor enhancements * fixes and enhancements * solve conflicts with main * Only stop wallet on rename and delete * fix: status toggle * minor enhancement * Monero.com fixes * bump monero_c commit * update sp_scanner to include windows and linux * merge main * Update macOS build guide. Change brew dependencies for build unbound locally. * fix: Tron file write, build scripts * - merge linux with Monero Dart - Temporarily disable Monero * fix other issues with linux * linux ci fix build script * Update pr_test_build_linux.yml install required packages * add linux desktop dependencies * don't use apk in linux build releases * don't copy the file to test-apk * fix linux runtime issues * remove libc++_shared.so * fix issues with linux * prepare both android and linux (because otherwise it will fail) * ci script updates * run apt update * bump image to ubuntu 22.04 note: remember to put it down later * bump python version * remove some dependencies * remove unused import * add missing dependencies * fix dependencies * some fixes * remove print [skip ci] * Add back RunnerBase.entitlements minor fixes [skip ci] * fix memory leak / infinite recurrsion when opening xmr wallet * url_launcher_linux: 3.1.1 # https://github.com/flutter/flutter/issues/153083 * fix conflicts with main * handle walletKeysFile with encryptionUtils * update app version [skip ci] * add wownero [skip ci] --------- Co-authored-by: OmarHatem Co-authored-by: Rafael Saes Co-authored-by: M Co-authored-by: Konstantin Ullrich --- .github/workflows/cache_dependencies.yml | 2 +- ...st_build.yml => pr_test_build_android.yml} | 4 +- .github/workflows/pr_test_build_linux.yml | 186 ++++++++++++ .gitignore | 2 + .metadata | 6 + build-guide-linux.md | 176 ++++++++++++ com.cakewallet.CakeWallet.yml | 35 +++ configure_cake_wallet.sh | 14 +- cw_bitcoin/lib/bitcoin_wallet.dart | 31 +- .../bitcoin_wallet_creation_credentials.dart | 2 + cw_bitcoin/lib/bitcoin_wallet_service.dart | 10 +- .../lib/electrum_transaction_history.dart | 11 +- cw_bitcoin/lib/electrum_wallet.dart | 22 +- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 5 +- cw_bitcoin/lib/litecoin_wallet.dart | 36 ++- cw_bitcoin/lib/litecoin_wallet_service.dart | 84 +++--- cw_bitcoin/pubspec.lock | 25 ++ .../lib/src/bitcoin_cash_wallet.dart | 23 +- ...coin_cash_wallet_creation_credentials.dart | 4 +- .../lib/src/bitcoin_cash_wallet_service.dart | 27 +- cw_core/lib/encryption_file_utils.dart | 42 +++ cw_core/lib/wallet_base.dart | 2 + cw_core/lib/wallet_keys_file.dart | 31 +- cw_core/pubspec.lock | 33 +++ cw_core/pubspec.yaml | 5 + .../lib/ethereum_transaction_history.dart | 1 + cw_ethereum/lib/ethereum_wallet.dart | 27 +- cw_ethereum/lib/ethereum_wallet_service.dart | 15 +- cw_evm/lib/evm_chain_transaction_history.dart | 10 +- cw_evm/lib/evm_chain_wallet.dart | 21 +- ...evm_chain_wallet_creation_credentials.dart | 4 +- cw_evm/lib/evm_chain_wallet_service.dart | 3 +- cw_evm/lib/file.dart | 39 --- cw_evm/pubspec.yaml | 1 + cw_haven/lib/haven_wallet.dart | 9 +- cw_haven/pubspec.lock | 33 +++ cw_monero/.metadata | 3 + cw_monero/example/linux/.gitignore | 1 + cw_monero/example/linux/CMakeLists.txt | 138 +++++++++ .../example/linux/flutter/CMakeLists.txt | 88 ++++++ .../flutter/generated_plugin_registrant.cc | 15 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 24 ++ cw_monero/example/linux/main.cc | 6 + cw_monero/example/linux/my_application.cc | 104 +++++++ cw_monero/example/linux/my_application.h | 18 ++ cw_monero/lib/api/account_list.dart | 1 - cw_monero/lib/api/wallet_manager.dart | 2 + cw_monero/lib/monero_wallet.dart | 10 +- cw_monero/lib/monero_wallet_service.dart | 57 +++- cw_monero/linux/CMakeLists.txt | 270 ++++++++++++++++++ cw_monero/linux/cw_monero_plugin.cc | 70 +++++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 23 ++ .../include/cw_monero/cw_monero_plugin.h | 26 ++ cw_monero/pubspec.lock | 37 ++- cw_nano/lib/file.dart | 39 --- cw_nano/lib/nano_transaction_history.dart | 22 +- cw_nano/lib/nano_wallet.dart | 32 ++- cw_nano/lib/nano_wallet_service.dart | 17 +- cw_nano/pubspec.lock | 33 +++ .../lib/polygon_transaction_history.dart | 1 + cw_polygon/lib/polygon_wallet.dart | 30 +- cw_polygon/lib/polygon_wallet_service.dart | 15 +- cw_solana/lib/file.dart | 39 --- cw_solana/lib/solana_transaction_history.dart | 10 +- cw_solana/lib/solana_wallet.dart | 34 ++- .../solana_wallet_creation_credentials.dart | 4 +- cw_solana/lib/solana_wallet_service.dart | 15 +- cw_tron/lib/file.dart | 39 --- cw_tron/lib/tron_transaction_history.dart | 10 +- cw_tron/lib/tron_wallet.dart | 27 +- .../lib/tron_wallet_creation_credentials.dart | 4 +- cw_tron/lib/tron_wallet_service.dart | 15 +- cw_wownero/lib/wownero_wallet.dart | 7 +- cw_wownero/lib/wownero_wallet_service.dart | 12 +- cw_wownero/pubspec.lock | 33 +++ lib/bitcoin/cw_bitcoin.dart | 12 +- lib/bitcoin_cash/cw_bitcoin_cash.dart | 7 +- lib/core/backup_service.dart | 2 +- lib/core/wallet_creation_service.dart | 32 ++- lib/core/wallet_loading_service.dart | 19 +- lib/di.dart | 86 +++++- lib/entities/load_current_wallet.dart | 7 +- lib/ethereum/cw_ethereum.dart | 7 +- lib/main.dart | 4 +- lib/nano/cw_nano.dart | 4 +- lib/polygon/cw_polygon.dart | 7 +- .../on_authentication_state_change.dart | 3 +- lib/router.dart | 79 +++-- lib/routes.dart | 2 + lib/solana/cw_solana.dart | 7 +- lib/src/screens/auth/auth_page.dart | 15 +- .../desktop_action_button.dart | 78 ++--- .../desktop_wallet_selection_dropdown.dart | 28 +- .../screens/new_wallet/new_wallet_page.dart | 94 +++++- .../wallet_restore_from_keys_form.dart | 49 +++- .../wallet_restore_from_seed_form.dart | 49 +++- .../screens/restore/wallet_restore_page.dart | 10 +- lib/src/screens/root/root.dart | 1 + .../settings/security_backup_page.dart | 19 +- lib/src/screens/wallet/wallet_edit_page.dart | 49 +++- .../screens/wallet_list/wallet_list_page.dart | 17 ++ .../wallet_unlock_arguments.dart | 17 ++ .../wallet_unlock/wallet_unlock_page.dart | 238 +++++++++++++++ lib/src/widgets/address_text_field.dart | 7 +- lib/store/settings_store.dart | 1 + lib/tron/cw_tron.dart | 8 +- lib/utils/package_info.dart | 2 +- lib/view_model/wallet_creation_vm.dart | 18 ++ .../wallet_list/wallet_edit_view_model.dart | 5 +- lib/view_model/wallet_new_vm.dart | 18 +- lib/view_model/wallet_restore_view_model.dart | 2 +- .../wallet_unlock_loadable_view_model.dart | 63 ++++ .../wallet_unlock_verifiable_view_model.dart | 60 ++++ lib/view_model/wallet_unlock_view_model.dart | 11 + linux/.gitignore | 1 + linux/CMakeLists.txt | 144 ++++++++++ linux/com.cakewallet.CakeWallet.desktop | 9 + linux/flutter/CMakeLists.txt | 88 ++++++ linux/flutter/generated_plugin_registrant.cc | 27 ++ linux/flutter/generated_plugin_registrant.h | 15 + linux/flutter/generated_plugins.cmake | 28 ++ linux/main.cc | 6 + linux/my_application.cc | 104 +++++++ linux/my_application.h | 18 ++ macos/CakeWallet/decrypt.swift | 16 ++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 - macos/Runner/RunnerBase.entitlements | 2 +- model_generator.sh | 1 + pubspec_base.yaml | 4 +- res/values/strings_ar.arb | 6 + res/values/strings_bg.arb | 6 + res/values/strings_cs.arb | 6 + res/values/strings_de.arb | 6 + res/values/strings_en.arb | 6 + res/values/strings_es.arb | 6 + res/values/strings_fr.arb | 6 + res/values/strings_ha.arb | 6 + res/values/strings_hi.arb | 6 + res/values/strings_hr.arb | 6 + res/values/strings_id.arb | 6 + res/values/strings_it.arb | 6 + res/values/strings_ja.arb | 6 + res/values/strings_ko.arb | 6 + res/values/strings_my.arb | 6 + res/values/strings_nl.arb | 6 + res/values/strings_pl.arb | 6 + res/values/strings_pt.arb | 6 + res/values/strings_ru.arb | 6 + res/values/strings_th.arb | 6 + res/values/strings_tl.arb | 6 + res/values/strings_tr.arb | 6 + res/values/strings_uk.arb | 6 + res/values/strings_ur.arb | 6 + res/values/strings_yo.arb | 6 + res/values/strings_zh.arb | 6 + scripts/android/shell.nix | 16 -- scripts/ios/gen_framework.sh | 2 +- scripts/linux/app_config.sh | 25 ++ scripts/linux/app_env.fish | 35 +++ scripts/linux/app_env.sh | 35 +++ scripts/linux/build_all.sh | 3 + scripts/linux/build_boost.sh | 36 +++ scripts/linux/build_expat.sh | 20 ++ scripts/linux/build_iconv.sh | 22 ++ scripts/linux/build_monero.sh | 40 +++ scripts/linux/build_monero_all.sh | 21 ++ scripts/linux/build_openssl.sh | 19 ++ scripts/linux/build_sodium.sh | 19 ++ scripts/linux/build_unbound.sh | 25 ++ scripts/linux/build_zmq.sh | 17 ++ scripts/linux/cakewallet.sh | 4 + scripts/linux/config.sh | 13 + scripts/linux/gcc10.nix | 12 + scripts/linux/setup.sh | 9 + tool/configure.dart | 34 +-- 178 files changed, 3955 insertions(+), 563 deletions(-) rename .github/workflows/{pr_test_build.yml => pr_test_build_android.yml} (98%) create mode 100644 .github/workflows/pr_test_build_linux.yml create mode 100644 build-guide-linux.md create mode 100644 com.cakewallet.CakeWallet.yml create mode 100644 cw_core/lib/encryption_file_utils.dart delete mode 100644 cw_evm/lib/file.dart create mode 100644 cw_monero/example/linux/.gitignore create mode 100644 cw_monero/example/linux/CMakeLists.txt create mode 100644 cw_monero/example/linux/flutter/CMakeLists.txt create mode 100644 cw_monero/example/linux/flutter/generated_plugin_registrant.cc create mode 100644 cw_monero/example/linux/flutter/generated_plugin_registrant.h create mode 100644 cw_monero/example/linux/flutter/generated_plugins.cmake create mode 100644 cw_monero/example/linux/main.cc create mode 100644 cw_monero/example/linux/my_application.cc create mode 100644 cw_monero/example/linux/my_application.h create mode 100644 cw_monero/linux/CMakeLists.txt create mode 100644 cw_monero/linux/cw_monero_plugin.cc create mode 100644 cw_monero/linux/flutter/generated_plugin_registrant.cc create mode 100644 cw_monero/linux/flutter/generated_plugin_registrant.h create mode 100644 cw_monero/linux/flutter/generated_plugins.cmake create mode 100644 cw_monero/linux/include/cw_monero/cw_monero_plugin.h delete mode 100644 cw_nano/lib/file.dart delete mode 100644 cw_solana/lib/file.dart delete mode 100644 cw_tron/lib/file.dart create mode 100644 lib/src/screens/wallet_unlock/wallet_unlock_arguments.dart create mode 100644 lib/src/screens/wallet_unlock/wallet_unlock_page.dart create mode 100644 lib/view_model/wallet_unlock_loadable_view_model.dart create mode 100644 lib/view_model/wallet_unlock_verifiable_view_model.dart create mode 100644 lib/view_model/wallet_unlock_view_model.dart create mode 100644 linux/.gitignore create mode 100644 linux/CMakeLists.txt create mode 100644 linux/com.cakewallet.CakeWallet.desktop create mode 100644 linux/flutter/CMakeLists.txt create mode 100644 linux/flutter/generated_plugin_registrant.cc create mode 100644 linux/flutter/generated_plugin_registrant.h create mode 100644 linux/flutter/generated_plugins.cmake create mode 100644 linux/main.cc create mode 100644 linux/my_application.cc create mode 100644 linux/my_application.h create mode 100644 macos/CakeWallet/decrypt.swift delete mode 100644 scripts/android/shell.nix create mode 100755 scripts/linux/app_config.sh create mode 100644 scripts/linux/app_env.fish create mode 100755 scripts/linux/app_env.sh create mode 100755 scripts/linux/build_all.sh create mode 100755 scripts/linux/build_boost.sh create mode 100755 scripts/linux/build_expat.sh create mode 100755 scripts/linux/build_iconv.sh create mode 100755 scripts/linux/build_monero.sh create mode 100755 scripts/linux/build_monero_all.sh create mode 100755 scripts/linux/build_openssl.sh create mode 100755 scripts/linux/build_sodium.sh create mode 100755 scripts/linux/build_unbound.sh create mode 100755 scripts/linux/build_zmq.sh create mode 100755 scripts/linux/cakewallet.sh create mode 100755 scripts/linux/config.sh create mode 100644 scripts/linux/gcc10.nix create mode 100755 scripts/linux/setup.sh diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index e9c53c00f..cca5bb4bf 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: "11.x" + java-version: "17.x" - name: Configure placeholder git details run: | git config --global user.email "CI@cakewallet.com" diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build_android.yml similarity index 98% rename from .github/workflows/pr_test_build.yml rename to .github/workflows/pr_test_build_android.yml index 4c46137ac..ea8770860 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -53,7 +53,9 @@ jobs: channel: stable - name: Install package dependencies - run: sudo apt-get install -y curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang + run: | + sudo apt update + sudo apt-get install -y curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang - name: Execute Build and Setup Commands run: | diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml new file mode 100644 index 000000000..12c930120 --- /dev/null +++ b/.github/workflows/pr_test_build_linux.yml @@ -0,0 +1,186 @@ +name: PR Test Build linux + +on: + pull_request: + branches: [main] + workflow_dispatch: + inputs: + branch: + description: "Branch name to build" + required: true + default: "main" + +jobs: + PR_test_build: + runs-on: ubuntu-20.04 + env: + STORE_PASS: test@cake_wallet + KEY_PASS: test@cake_wallet + PR_NUMBER: ${{ github.event.number }} + + steps: + - name: is pr + if: github.event_name == 'pull_request' + run: echo "BRANCH_NAME=${GITHUB_HEAD_REF}" >> $GITHUB_ENV + + - name: is not pr + if: github.event_name != 'pull_request' + run: echo "BRANCH_NAME=${{ github.event.inputs.branch }}" >> $GITHUB_ENVg + + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: "17.x" + - name: Configure placeholder git details + run: | + git config --global user.email "CI@cakewallet.com" + git config --global user.name "Cake Github Actions" + - name: Flutter action + uses: subosito/flutter-action@v1 + with: + flutter-version: "3.19.6" + channel: stable + + - name: Install package dependencies + run: | + sudo apt update + sudo apt-get install -y curl unzip automake build-essential file pkg-config git python-is-python3 libtool libtinfo5 cmake clang + + - name: Install desktop dependencies + run: | + sudo apt update + sudo apt install -y ninja-build libgtk-3-dev gperf + - name: Execute Build and Setup Commands + run: | + sudo mkdir -p /opt/android + sudo chown $USER /opt/android + cd /opt/android + -y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + cargo install cargo-ndk + git clone https://github.com/cake-tech/cake_wallet.git --branch ${{ env.BRANCH_NAME }} + cd scripts && ./gen_android_manifest.sh && cd .. + cd cake_wallet/scripts/android/ + source ./app_env.sh cakewallet + ./app_config.sh + cd ../../.. + cd cake_wallet/scripts/linux/ + source ./app_env.sh cakewallet + ./app_config.sh + cd ../../.. + + - name: Cache Externals + id: cache-externals + uses: actions/cache@v3 + with: + path: | + /opt/android/cake_wallet/cw_haven/android/.cxx + /opt/android/cake_wallet/scripts/monero_c/release + key: linux-${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh') }} + + - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} + name: Generate Externals + run: | + cd /opt/android/cake_wallet/scripts/linux/ + source ./app_env.sh cakewallet + ./build_monero_all.sh + + - name: Install Flutter dependencies + run: | + cd /opt/android/cake_wallet + flutter pub get + + - name: Generate localization + run: | + cd /opt/android/cake_wallet + flutter packages pub run tool/generate_localization.dart + + - name: Build generated code + run: | + cd /opt/android/cake_wallet + ./model_generator.sh + + - name: Add secrets + run: | + cd /opt/android/cake_wallet + touch lib/.secrets.g.dart + touch cw_evm/lib/.secrets.g.dart + touch cw_solana/lib/.secrets.g.dart + touch cw_core/lib/.secrets.g.dart + touch cw_nano/lib/.secrets.g.dart + touch cw_tron/lib/.secrets.g.dart + echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart + echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart + echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart + echo "const walletSalt = '${{ secrets.WALLET_SALT }}';" >> lib/.secrets.g.dart + echo "const shortKey = '${{ secrets.SHORT_KEY }}';" >> lib/.secrets.g.dart + echo "const backupSalt = '${{ secrets.BACKUP_SALT }}';" >> lib/.secrets.g.dart + echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart + echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart + echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart + echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart + echo "const moonPayApiKey = '${{ secrets.MOON_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const moonPaySecretKey = '${{ secrets.MOON_PAY_SECRET_KEY }}';" >> lib/.secrets.g.dart + echo "const sideShiftAffiliateId = '${{ secrets.SIDE_SHIFT_AFFILIATE_ID }}';" >> lib/.secrets.g.dart + echo "const simpleSwapApiKey = '${{ secrets.SIMPLE_SWAP_API_KEY }}';" >> lib/.secrets.g.dart + echo "const simpleSwapApiKeyDesktop = '${{ secrets.SIMPLE_SWAP_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const onramperApiKey = '${{ secrets.ONRAMPER_API_KEY }}';" >> lib/.secrets.g.dart + echo "const anypayToken = '${{ secrets.ANY_PAY_TOKEN }}';" >> lib/.secrets.g.dart + echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart + echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart + echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart + echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart + echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart + echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart + echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart + echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart + echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart + echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart + echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart + echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + + - name: Rename app + run: | + echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties + + - name: Build + run: | + cd /opt/android/cake_wallet + flutter build linux --release + + - name: Prepare release zip file + run: | + cd /opt/android/cake_wallet/build/linux/x64/release + zip -r ${{env.BRANCH_NAME}}.zip bundle + + - name: Upload Artifact + uses: kittaakos/upload-artifact-as-is@v0 + with: + path: /opt/android/cake_wallet/build/linux/x64/release/${{env.BRANCH_NAME}}.zip + + - name: Send Test APK + continue-on-error: true + uses: adrey/slack-file-upload-action@1.0.5 + with: + token: ${{ secrets.SLACK_APP_TOKEN }} + path: /opt/android/cake_wallet/build/linux/x64/release/${{env.BRANCH_NAME}}.zip + channel: ${{ secrets.SLACK_APK_CHANNEL }} + title: "${{ env.BRANCH_NAME }}_linux.zip" + filename: ${{ env.BRANCH_NAME }}_linux.zip + initial_comment: ${{ github.event.head_commit.message }} diff --git a/.gitignore b/.gitignore index 77441e66f..8336ca512 100644 --- a/.gitignore +++ b/.gitignore @@ -160,6 +160,8 @@ macos/Runner/Release.entitlements macos/Runner/Runner.entitlements lib/core/secure_storage.dart +lib/core/secure_storage.dart + macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/.metadata b/.metadata index 7d00ca21a..c7b8dc9f8 100644 --- a/.metadata +++ b/.metadata @@ -18,6 +18,12 @@ migration: - platform: windows create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: macos + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: linux + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 # User provided section diff --git a/build-guide-linux.md b/build-guide-linux.md new file mode 100644 index 000000000..e0158945b --- /dev/null +++ b/build-guide-linux.md @@ -0,0 +1,176 @@ +# Building CakeWallet for Linux + +## Requirements and Setup + +The following are the system requirements to build CakeWallet for your Linux device. + +``` +Ubuntu >= 16.04 +Flutter 3.10.x +``` + +## Building CakeWallet on Linux + +These steps will help you configure and execute a build of CakeWallet from its source code. + +### 1. Installing Package Dependencies + +CakeWallet requires some packages to be install on your build system. You may easily install them on your build system with the following command: + +`$ sudo apt install build-essential cmake pkg-config git curl autoconf libtool` + +> [!WARNING] +> +> ### Check gcc version +> +> It is needed to use gcc 10 or 9 to successfully link dependencies with flutter.\ +> To check what gcc version you are using: +> +> ```bash +> $ gcc --version +> $ g++ --version +> ``` +> +> If you are using gcc version newer than 10, then you need to downgrade to version 10.4.0: +> +> ```bash +> $ sudo apt install gcc-10 g++-10 +> $ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10 +> $ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10 +> ``` + +> [!NOTE] +> +> Alternatively, you can use the [nix-shell](https://nixos.org/) with the `gcc10.nix` file\ +> present on `scripts/linux` like so: +> ```bash +> $ nix-shell gcc10.nix +> ``` +> This will get you in a nix environment with all the required dependencies that you can use to build the software from,\ +> and it works in any linux distro. + +### 2. Installing Flutter + +Need to install flutter. For this please check section [How to install flutter on Linux](https://docs.flutter.dev/get-started/install/linux). + +### 3. Verify Installations + +Verify that the Flutter have been correctly installed on your system with the following command: + +`$ flutter doctor` + +The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. + +``` +Doctor summary (to see all details, run flutter doctor -v): +[✓] Flutter (Channel stable, 3.10.x, on Linux, locale en_US.UTF-8) +``` + +### 4. Acquiring the CakeWallet Source Code + +Download CakeWallet source code + +`$ git clone https://github.com/cake-tech/cake_wallet.git --branch linux/password-direct-input` + +Proceed into the source code before proceeding with the next steps: + +`$ cd cake_wallet/scripts/linux/` + +To configure some project properties run: + +`$ ./cakewallet.sh` + +Build the Monero libraries and their dependencies: + +`$ ./build_all.sh` + +Now the dependencies need to be copied into the CakeWallet project with this command: + +`$ ./setup.sh` + +It is now time to change back to the base directory of the CakeWallet source code: + +`$ cd ../../` + +Install Flutter package dependencies with this command: + +`$ flutter pub get` + +> #### If you will get an error like: +> +> ``` +> The plugin `cw_shared_external` requires your app to be migrated to the Android embedding v2. Follow the steps on the migration doc above and re-run +> this command. +> ``` +> +> Then need to config Android project settings. For this open `scripts/android` (`$ cd scripts/android`) directory and run followed commands: +> +> ``` +> $ source ./app_env.sh cakewallet +> $ ./app_config.sh +> $ cd ../.. +> ``` +> +> Then re-configure Linux project again. For this open `scripts/linux` (`$cd scripts/linux`) directory and run: +> `$ ./cakewallet.sh` +> and back to project root directory: +> `$ cd ../..` +> and fetch dependecies again +> `$ flutter pub get` + +Your CakeWallet binary will be built with some specific keys for iterate with 3rd party services. You may generate these secret keys placeholders with the following command: + +`$ flutter packages pub run tool/generate_new_secrets.dart` + +We will generate mobx models for the project. + +`$ ./model_generator.sh` + +Then we need to generate localization files. + +`$ flutter packages pub run tool/generate_localization.dart` + +### 5. Build! + +`$ flutter build linux --release` + +Path to executable file will be: + +`build/linux/x64/release/bundle/cake_wallet` + +> ### Troubleshooting +> +> If you got an error while building the application with `$ flutter build linux --release` command, add `-v` argument to the command (`$ flutter build linux -v --release`) to get details.\ +> If you got in flutter build logs: undefined reference to `hid_free_enumeration`, or another error with undefined reference to `hid_*`, then rebuild monero lib without hidapi lib. Check does exists `libhidapi-dev` in your scope and remove it from your scope for build without it. + +# Flatpak + +For package the built application into flatpak you need fistly to install `flatpak` and `flatpak-builder`: + +`$ sudo apt install flatpak flatpak-builder` + +Then need to [add flathub](https://flatpak.org/setup/Ubuntu) (or just `$ flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo`). Then need to install freedesktop runtime and sdk: + +`$ flatpak install flathub org.freedesktop.Platform//22.08 org.freedesktop.Sdk//22.08` + +To build with using of `flatpak-build` directory run next: + +`$ flatpak-builder --force-clean flatpak-build com.cakewallet.CakeWallet.yml` + +And then export bundle: + +`$ flatpak build-export export flatpak-build` + +`$ flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet` + +Result file: `cake_wallet.flatpak` should be generated in current directory. + +For install generated flatpak file use: + +`$ flatpak --user install cake_wallet.flatpak` + +For run the installed application run: + +`$ flatpak run com.cakewallet.CakeWallet` + +Copyright (c) 2023 Cake Technologies LLC. diff --git a/com.cakewallet.CakeWallet.yml b/com.cakewallet.CakeWallet.yml new file mode 100644 index 000000000..83efa1388 --- /dev/null +++ b/com.cakewallet.CakeWallet.yml @@ -0,0 +1,35 @@ +app-id: com.cakewallet.CakeWallet +runtime: org.freedesktop.Platform +runtime-version: '22.08' +sdk: org.freedesktop.Sdk +command: cake_wallet +separate-locales: false +finish-args: + - --share=ipc + - --socket=fallback-x11 + - --socket=wayland + - --device=dri + - --socket=pulseaudio + - --share=network + - --filesystem=home +modules: + - name: cake_wallet + buildsystem: simple + only-arches: + - x86_64 + build-commands: + - "cp -R bundle /app/cake_wallet" + - "chmod +x /app/cake_wallet/cake_wallet" + - "mkdir -p /app/bin" + - "ln -s /app/cake_wallet/cake_wallet /app/bin/cake_wallet" + - "mkdir -p /app/share/icons/hicolor/scalable/apps" + - "cp cakewallet_icon_180.png /app/share/icons/hicolor/scalable/apps/com.cakewallet.CakeWallet.png" + - "mkdir -p /app/share/applications" + - "cp com.cakewallet.CakeWallet.desktop /app/share/applications" + sources: + - type: dir + path: build/linux/x64/release + - type: file + path: assets/images/cakewallet_icon_180.png + - type: file + path: linux/com.cakewallet.CakeWallet.desktop diff --git a/configure_cake_wallet.sh b/configure_cake_wallet.sh index 0539221a3..90ce1c446 100755 --- a/configure_cake_wallet.sh +++ b/configure_cake_wallet.sh @@ -3,12 +3,13 @@ IOS="ios" ANDROID="android" MACOS="macos" +LINUX="linux" -PLATFORMS=($IOS $ANDROID $MACOS) +PLATFORMS=($IOS $ANDROID $MACOS $LINUX) PLATFORM=$1 if ! [[ " ${PLATFORMS[*]} " =~ " ${PLATFORM} " ]]; then - echo "specify platform: ./configure_cake_wallet.sh ios|android|macos" + echo "specify platform: ./configure_cake_wallet.sh ios|android|macos|linux" exit 1 fi @@ -27,9 +28,14 @@ if [ "$PLATFORM" == "$ANDROID" ]; then cd scripts/android fi +if [ "$PLATFORM" == "$LINUX" ]; then + echo "Configuring for linux" + cd scripts/linux +fi + source ./app_env.sh cakewallet ./app_config.sh cd ../.. && flutter pub get -#flutter packages pub run tool/generate_localization.dart +flutter packages pub run tool/generate_localization.dart ./model_generator.sh -#cd macos && pod install \ No newline at end of file +#cd macos && pod install diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 7b8250541..e2e537ee8 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -5,9 +5,10 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_bitcoin/electrum_derivations.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/psbt_transaction_builder.dart'; @@ -30,6 +31,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required EncryptionFileUtils encryptionFileUtils, Uint8List? seedBytes, String? mnemonic, String? xpub, @@ -58,6 +60,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: seedBytes, + encryptionFileUtils: encryptionFileUtils, currency: networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, alwaysScan: alwaysScan, @@ -90,6 +93,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required EncryptionFileUtils encryptionFileUtils, String? passphrase, String? addressPageType, BasedUtxoNetwork? network, @@ -124,6 +128,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddresses: initialSilentAddresses, initialSilentAddressIndex: initialSilentAddressIndex, initialBalance: initialBalance, + encryptionFileUtils: encryptionFileUtils, seedBytes: seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, @@ -137,6 +142,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required String password, + required EncryptionFileUtils encryptionFileUtils, required bool alwaysScan, }) async { final network = walletInfo.network != null @@ -148,7 +154,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ElectrumWalletSnapshot? snp = null; try { - snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network); + snp = await ElectrumWalletSnapshot.load( + encryptionFileUtils, + name, + walletInfo.type, + password, + network, + ); } catch (e) { if (!hasKeysFile) rethrow; } @@ -156,10 +168,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final WalletKeysData keysData; // Migrate wallet from the old scheme to then new .keys file scheme if (!hasKeysFile) { - keysData = - WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase); + keysData = WalletKeysData( + mnemonic: snp!.mnemonic, + xPub: snp.xpub, + passphrase: snp.passphrase, + ); } else { - keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); } walletInfo.derivationInfo ??= DerivationInfo(); @@ -198,6 +218,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddresses: snp?.silentAddresses, initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, initialBalance: snp?.balance, + encryptionFileUtils: encryptionFileUtils, seedBytes: seedBytes, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 915d7cc10..91b8e4ae2 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -6,11 +6,13 @@ class BitcoinNewWalletCredentials extends WalletCredentials { BitcoinNewWalletCredentials( {required String name, WalletInfo? walletInfo, + String? password, DerivationType? derivationType, String? derivationPath}) : super( name: name, walletInfo: walletInfo, + password: password, ); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index cf93aa29d..d6d97f3de 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -3,6 +3,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.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/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_service.dart'; @@ -19,11 +20,12 @@ class BitcoinWalletService extends WalletService< BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromHardware> { - BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan); + BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; final bool alwaysScan; + final bool isDirect; @override WalletType getType() => WalletType.bitcoin; @@ -40,6 +42,7 @@ class BitcoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, network: network, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.save(); @@ -63,6 +66,7 @@ class BitcoinWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); saveBackup(name); @@ -75,6 +79,7 @@ class BitcoinWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); return wallet; @@ -99,6 +104,7 @@ class BitcoinWalletService extends WalletService< walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await currentWallet.renameWalletFiles(newName); @@ -125,6 +131,7 @@ class BitcoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, networkParam: network, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.save(); await wallet.init(); @@ -153,6 +160,7 @@ class BitcoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, network: network, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index a7de414e4..806f813dd 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -6,6 +7,8 @@ import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; part 'electrum_transaction_history.g.dart'; @@ -15,13 +18,15 @@ class ElectrumTransactionHistory = ElectrumTransactionHistoryBase with _$Electru abstract class ElectrumTransactionHistoryBase extends TransactionHistoryBase with Store { - ElectrumTransactionHistoryBase({required this.walletInfo, required String password}) + ElectrumTransactionHistoryBase( + {required this.walletInfo, required String password, required this.encryptionFileUtils}) : _password = password, _height = 0 { transactions = ObservableMap(); } final WalletInfo walletInfo; + final EncryptionFileUtils encryptionFileUtils; String _password; int _height; @@ -44,7 +49,7 @@ abstract class ElectrumTransactionHistoryBase txjson[tx.key] = tx.value.toJson(); } final data = json.encode({'height': _height, 'transactions': txjson}); - await writeData(path: path, password: _password, data: data); + await encryptionFileUtils.write(path: path, password: _password, data: data); } catch (e) { print('Error while save bitcoin transaction history: ${e.toString()}'); } @@ -58,7 +63,7 @@ abstract class ElectrumTransactionHistoryBase Future> _read() async { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final path = '$dirPath/$transactionsHistoryFileName'; - final content = await read(path: path, password: _password); + final content = await encryptionFileUtils.read(path: path, password: _password); return json.decode(content) as Map; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index e1b038beb..501d94e54 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -5,6 +5,7 @@ import 'dart:isolate'; import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; @@ -32,7 +33,6 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; @@ -58,6 +58,7 @@ abstract class ElectrumWalletBase required WalletInfo walletInfo, required Box unspentCoinsInfo, required this.network, + required this.encryptionFileUtils, String? xpub, String? mnemonic, Uint8List? seedBytes, @@ -92,7 +93,11 @@ abstract class ElectrumWalletBase super(walletInfo) { this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; - transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); + transactionHistory = ElectrumTransactionHistory( + walletInfo: walletInfo, + password: password, + encryptionFileUtils: encryptionFileUtils, + ); reaction((_) => syncStatus, _syncStatusReaction); } @@ -127,6 +132,8 @@ abstract class ElectrumWalletBase final String? _mnemonic; Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); + + final EncryptionFileUtils encryptionFileUtils; final String? passphrase; @override @@ -167,6 +174,9 @@ abstract class ElectrumWalletBase WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, xPub: xpub, passphrase: passphrase); + @override + String get password => _password; + BasedUtxoNetwork network; @override @@ -455,7 +465,6 @@ abstract class ElectrumWalletBase } } - node!.isElectrs = false; node!.save(); return node!.isElectrs!; @@ -1130,12 +1139,12 @@ abstract class ElectrumWalletBase @override Future save() async { if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { - await saveKeysFile(_password); - saveKeysFile(_password, true); + await saveKeysFile(_password, encryptionFileUtils); + saveKeysFile(_password, encryptionFileUtils, true); } final path = await makePath(); - await write(path: path, password: _password, data: toJSON()); + await encryptionFileUtils.write(path: path, password: _password, data: toJSON()); await transactionHistory.save(); } @@ -2258,4 +2267,3 @@ class UtxoDetails { required this.spendsUnconfirmedTX, }); } - diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 082460f72..fa58be238 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; @@ -51,9 +52,9 @@ class ElectrumWalletSnapshot { String? derivationPath; static Future load( - String name, WalletType type, String password, BasedUtxoNetwork network) async { + EncryptionFileUtils encryptionFileUtils, String name, WalletType type, String password, BasedUtxoNetwork network) async { final path = await pathForWallet(name: name, type: type); - final jsonSource = await read(path: path, password: password); + final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; final addressesTmp = data['addresses'] as List? ?? []; final mnemonic = data['mnemonic'] as String?; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 64e53ca5d..d8c04dba6 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -4,13 +4,14 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; -import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_priority.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'; @@ -28,6 +29,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required Uint8List seedBytes, + required EncryptionFileUtils encryptionFileUtils, String? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, @@ -42,6 +44,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: seedBytes, + encryptionFileUtils: encryptionFileUtils, currency: CryptoCurrency.ltc) { walletAddresses = LitecoinWalletAddresses( walletInfo, @@ -62,6 +65,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required EncryptionFileUtils encryptionFileUtils, String? passphrase, String? addressPageType, List? initialAddresses, @@ -89,6 +93,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, + encryptionFileUtils: encryptionFileUtils, seedBytes: seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, @@ -96,19 +101,24 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } - static Future open({ - required String name, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required String password, - }) async { + static Future open( + {required String name, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required String password, + required EncryptionFileUtils encryptionFileUtils}) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); ElectrumWalletSnapshot? snp = null; try { snp = await ElectrumWalletSnapshot.load( - name, walletInfo.type, password, LitecoinNetwork.mainnet); + encryptionFileUtils, + name, + walletInfo.type, + password, + LitecoinNetwork.mainnet, + ); } catch (e) { if (!hasKeysFile) rethrow; } @@ -119,7 +129,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { keysData = WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase); } else { - keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); } return LitecoinWallet( @@ -130,6 +145,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialAddresses: snp?.addresses, initialBalance: snp?.balance, seedBytes: await mnemonicToSeedBytes(keysData.mnemonic!), + encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 7025b72e5..a46b12a2e 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; @@ -16,11 +17,13 @@ import 'package:bip39/bip39.dart' as bip39; class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, - BitcoinRestoreWalletFromWIFCredentials,BitcoinNewWalletCredentials> { - LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); + BitcoinRestoreWalletFromWIFCredentials, + BitcoinNewWalletCredentials> { + LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; + final bool isDirect; @override WalletType getType() => WalletType.litecoin; @@ -28,12 +31,13 @@ class LitecoinWalletService extends WalletService< @override Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { final wallet = await LitecoinWalletBase.create( - mnemonic: await generateElectrumMnemonic(), - password: credentials.password!, - passphrase: credentials.passphrase, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); - + mnemonic: await generateElectrumMnemonic(), + password: credentials.password!, + passphrase: credentials.passphrase, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await wallet.save(); await wallet.init(); @@ -46,21 +50,29 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { - final walletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(name, getType()))!; + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; try { final wallet = await LitecoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await wallet.init(); saveBackup(name); return wallet; } catch (_) { await restoreWalletFilesFromBackup(name); final wallet = await LitecoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await wallet.init(); return wallet; } @@ -68,22 +80,23 @@ class LitecoinWalletService extends WalletService< @override Future remove(String wallet) async { - File(await pathForWalletDir(name: wallet, type: getType())) - .delete(recursive: true); - final walletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(wallet, getType()))!; + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); } @override Future rename(String currentName, String password, String newName) async { - final currentWalletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(currentName, getType()))!; + final currentWalletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; final currentWallet = await LitecoinWalletBase.open( - password: password, - name: currentName, - walletInfo: currentWalletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + password: password, + name: currentName, + walletInfo: currentWalletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); @@ -97,27 +110,30 @@ class LitecoinWalletService extends WalletService< @override Future restoreFromHardwareWallet(BitcoinNewWalletCredentials credentials) { - throw UnimplementedError("Restoring a Litecoin wallet from a hardware wallet is not yet supported!"); + throw UnimplementedError( + "Restoring a Litecoin wallet from a hardware wallet is not yet supported!"); } @override - Future restoreFromKeys( - BitcoinRestoreWalletFromWIFCredentials credentials, {bool? isTestnet}) async => + Future restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, + {bool? isTestnet}) async => throw UnimplementedError(); @override - Future restoreFromSeed( - BitcoinRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { + Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) { throw LitecoinMnemonicIsIncorrectException(); } final wallet = await LitecoinWalletBase.create( - password: credentials.password!, - passphrase: credentials.passphrase, - mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); + password: credentials.password!, + passphrase: credentials.passphrase, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await wallet.save(); await wallet.init(); return wallet; diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index be7862e26..12274c1e6 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -164,6 +164,15 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.2" + cake_backup: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" characters: dependency: transitive description: @@ -236,6 +245,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" cw_core: dependency: "direct main" description: @@ -837,6 +854,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 8323c01a8..a59569ae6 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -1,6 +1,7 @@ import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -28,6 +29,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required Uint8List seedBytes, + required EncryptionFileUtils encryptionFileUtils, BitcoinAddressType? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, @@ -42,7 +44,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: seedBytes, - currency: CryptoCurrency.bch) { + currency: CryptoCurrency.bch, + encryptionFileUtils: encryptionFileUtils) { walletAddresses = BitcoinCashWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -63,6 +66,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required EncryptionFileUtils encryptionFileUtils, String? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, @@ -76,6 +80,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: await MnemonicBip39.toSeed(mnemonic), + encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, @@ -87,6 +92,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required String password, + required EncryptionFileUtils encryptionFileUtils, }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); @@ -94,7 +100,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { try { snp = await ElectrumWalletSnapshot.load( - name, walletInfo.type, password, BitcoinCashNetwork.mainnet); + encryptionFileUtils, + name, + walletInfo.type, + password, + BitcoinCashNetwork.mainnet, + ); } catch (e) { if (!hasKeysFile) rethrow; } @@ -105,7 +116,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { keysData = WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase); } else { - keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); } return BitcoinCashWallet( @@ -135,6 +151,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { }).toList(), initialBalance: snp?.balance, seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!), + encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart index 72caa6c58..017040c5d 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart @@ -2,8 +2,8 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class BitcoinCashNewWalletCredentials extends WalletCredentials { - BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo}) - : super(name: name, walletInfo: walletInfo); + BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) + : super(name: name, walletInfo: walletInfo, password: password); } class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index 002e52c4f..a970be261 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:bip39/bip39.dart'; import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; @@ -16,10 +17,11 @@ class BitcoinCashWalletService extends WalletService< BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials, BitcoinCashNewWalletCredentials> { - BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); + BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; + final bool isDirect; @override WalletType getType() => WalletType.bitcoinCash; @@ -34,10 +36,11 @@ class BitcoinCashWalletService extends WalletService< final wallet = await BitcoinCashWalletBase.create( mnemonic: await MnemonicBip39.generate(strength: strength), - password: credentials.password!, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); - + password: credentials.password!, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await wallet.save(); await wallet.init(); @@ -54,7 +57,9 @@ class BitcoinCashWalletService extends WalletService< password: password, name: name, walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await wallet.init(); saveBackup(name); return wallet; @@ -64,7 +69,9 @@ class BitcoinCashWalletService extends WalletService< password: password, name: name, walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await wallet.init(); return wallet; } @@ -86,7 +93,8 @@ class BitcoinCashWalletService extends WalletService< password: password, name: currentName, walletInfo: currentWalletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect)); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); @@ -121,7 +129,8 @@ class BitcoinCashWalletService extends WalletService< password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect)); await wallet.save(); await wallet.init(); return wallet; diff --git a/cw_core/lib/encryption_file_utils.dart b/cw_core/lib/encryption_file_utils.dart new file mode 100644 index 000000000..1889c4389 --- /dev/null +++ b/cw_core/lib/encryption_file_utils.dart @@ -0,0 +1,42 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:cw_core/utils/file.dart' as file; +import 'package:cake_backup/backup.dart' as cwb; + +EncryptionFileUtils encryptionFileUtilsFor(bool direct) + => direct + ? XChaCha20EncryptionFileUtils() + : Salsa20EncryhptionFileUtils(); + +abstract class EncryptionFileUtils { + Future write({required String path, required String password, required String data}); + Future read({required String path, required String password}); +} + +class Salsa20EncryhptionFileUtils extends EncryptionFileUtils { + // Requires legacy complex key + iv as password + @override + Future write({required String path, required String password, required String data}) async + => await file.write(path: path, password: password, data: data); + + // Requires legacy complex key + iv as password + @override + Future read({required String path, required String password}) async + => await file.read(path: path, password: password); +} + +class XChaCha20EncryptionFileUtils extends EncryptionFileUtils { + @override + Future write({required String path, required String password, required String data}) async { + final encrypted = await cwb.encrypt(password, Uint8List.fromList(data.codeUnits)); + await File(path).writeAsBytes(encrypted); + } + + @override + Future read({required String path, required String password}) async { + final file = File(path); + final encrypted = await file.readAsBytes(); + final bytes = await cwb.decrypt(password, encrypted); + return String.fromCharCodes(bytes); + } +} \ No newline at end of file diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index a616b0bfd..f7af15224 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -84,6 +84,8 @@ abstract class WalletBase changePassword(String password); + String get password; + Future? updateBalance(); void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null; diff --git a/cw_core/lib/wallet_keys_file.dart b/cw_core/lib/wallet_keys_file.dart index 45539e09d..638cdc39d 100644 --- a/cw_core/lib/wallet_keys_file.dart +++ b/cw_core/lib/wallet_keys_file.dart @@ -3,10 +3,10 @@ import 'dart:developer' as dev; import 'dart:io'; import 'package:cw_core/balance.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; @@ -20,28 +20,32 @@ mixin WalletKeysFile makeKeysFilePath() async => "${await makePath()}.keys"; - Future saveKeysFile(String password, [bool isBackup = false]) async { + Future saveKeysFile(String password, EncryptionFileUtils encryptionFileUtils, + [bool isBackup = false]) async { try { final rootPath = await makeKeysFilePath(); final path = "$rootPath${isBackup ? ".backup" : ""}"; dev.log("Saving .keys file '$path'"); - await write(path: path, password: password, data: walletKeysData.toJSON()); + await encryptionFileUtils.write( + path: path, password: password, data: walletKeysData.toJSON()); } catch (_) {} } - static Future createKeysFile( - String name, WalletType type, String password, WalletKeysData walletKeysData, + static Future createKeysFile(String name, WalletType type, String password, + WalletKeysData walletKeysData, EncryptionFileUtils encryptionFileUtils, [bool withBackup = true]) async { try { final rootPath = await pathForWallet(name: name, type: type); final path = "$rootPath.keys"; dev.log("Saving .keys file '$path'"); - await write(path: path, password: password, data: walletKeysData.toJSON()); + await encryptionFileUtils.write( + path: path, password: password, data: walletKeysData.toJSON()); if (withBackup) { dev.log("Saving .keys.backup file '$path.backup'"); - await write(path: "$path.backup", password: password, data: walletKeysData.toJSON()); + await encryptionFileUtils.write( + path: "$path.backup", password: password, data: walletKeysData.toJSON()); } } catch (_) {} } @@ -55,14 +59,19 @@ mixin WalletKeysFile readKeysFile(String name, WalletType type, String password) async { + static Future readKeysFile( + String name, + WalletType type, + String password, + EncryptionFileUtils encryptionFileUtils, + ) async { final path = await pathForWallet(name: name, type: type); var readPath = "$path.keys"; try { if (!File(readPath).existsSync()) throw Exception("No .keys file found for $name $type"); - final jsonSource = await read(path: readPath, password: password); + final jsonSource = await encryptionFileUtils.read(path: readPath, password: password); final data = json.decode(jsonSource) as Map; return WalletKeysData.fromJSON(data); } catch (e) { @@ -72,12 +81,12 @@ mixin WalletKeysFile; final keysData = WalletKeysData.fromJSON(data); dev.log("Restoring .keys from .keys.backup"); - createKeysFile(name, type, password, keysData, false); + createKeysFile(name, type, password, keysData, encryptionFileUtils, false); return keysData; } } diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 518c71b94..e905af2d9 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -113,6 +113,15 @@ packages: url: "https://pub.dev" source: hosted version: "8.8.1" + cake_backup: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" characters: dependency: transitive description: @@ -169,6 +178,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" dart_style: dependency: transitive description: @@ -648,6 +673,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 0513b122c..4497a709d 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -19,6 +19,11 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.18.0 encrypt: ^5.0.1 + cake_backup: + git: + url: https://github.com/cake-tech/cake_backup.git + ref: main + version: 1.0.0 socks5_proxy: ^1.0.4 unorm_dart: ^0.3.0 # tor: diff --git a/cw_ethereum/lib/ethereum_transaction_history.dart b/cw_ethereum/lib/ethereum_transaction_history.dart index f774ae905..fbb8ab79d 100644 --- a/cw_ethereum/lib/ethereum_transaction_history.dart +++ b/cw_ethereum/lib/ethereum_transaction_history.dart @@ -7,6 +7,7 @@ class EthereumTransactionHistory extends EVMChainTransactionHistory { EthereumTransactionHistory({ required super.walletInfo, required super.password, + required super.encryptionFileUtils, }); @override diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index 7bcd55cf4..51aeab5e1 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_direction.dart'; @@ -16,7 +17,6 @@ import 'package:cw_evm/evm_chain_transaction_info.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_wallet.dart'; import 'package:cw_evm/evm_erc20_balance.dart'; -import 'package:cw_evm/file.dart'; class EthereumWallet extends EVMChainWallet { EthereumWallet({ @@ -26,6 +26,7 @@ class EthereumWallet extends EVMChainWallet { super.mnemonic, super.initialBalance, super.privateKey, + required super.encryptionFileUtils, }) : super(nativeCurrency: CryptoCurrency.eth); @override @@ -117,18 +118,24 @@ class EthereumWallet extends EVMChainWallet { } @override - EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) { - return EthereumTransactionHistory(walletInfo: walletInfo, password: password); + EVMChainTransactionHistory setUpTransactionHistory( + WalletInfo walletInfo, String password, EncryptionFileUtils encryptionFileUtils) { + return EthereumTransactionHistory( + walletInfo: walletInfo, password: password, encryptionFileUtils: encryptionFileUtils); } - static Future open( - {required String name, required String password, required WalletInfo walletInfo}) async { + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + required EncryptionFileUtils encryptionFileUtils, + }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); Map? data; try { - final jsonSource = await read(path: path, password: password); + final jsonSource = await encryptionFileUtils.read(path: path, password: password); data = json.decode(jsonSource) as Map; } catch (e) { @@ -146,7 +153,12 @@ class EthereumWallet extends EVMChainWallet { keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); } else { - keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); } return EthereumWallet( @@ -156,6 +168,7 @@ class EthereumWallet extends EVMChainWallet { privateKey: keysData.privateKey, initialBalance: balance, client: EthereumClient(), + encryptionFileUtils: encryptionFileUtils, ); } } diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index c0d3df2d6..84fc0a277 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -1,4 +1,5 @@ import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -9,7 +10,7 @@ import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; import 'package:cw_evm/evm_chain_wallet_service.dart'; class EthereumWalletService extends EVMChainWalletService { - EthereumWalletService(super.walletInfoSource, {required this.client}); + EthereumWalletService(super.walletInfoSource, super.isDirect, {required this.client}); late EthereumClient client; @@ -27,6 +28,7 @@ class EthereumWalletService extends EVMChainWalletService { mnemonic: mnemonic, password: credentials.password!, client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -46,6 +48,7 @@ class EthereumWalletService extends EVMChainWalletService { name: name, password: password, walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -59,6 +62,7 @@ class EthereumWalletService extends EVMChainWalletService { name: name, password: password, walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); await wallet.save(); @@ -71,7 +75,11 @@ class EthereumWalletService extends EVMChainWalletService { final currentWalletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); final currentWallet = await EthereumWallet.open( - password: password, name: currentName, walletInfo: currentWalletInfo); + password: password, + name: currentName, + walletInfo: currentWalletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); @@ -97,6 +105,7 @@ class EthereumWalletService extends EVMChainWalletService { walletInfo: credentials.walletInfo!, password: credentials.password!, client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -114,6 +123,7 @@ class EthereumWalletService extends EVMChainWalletService { privateKey: credentials.privateKey, walletInfo: credentials.walletInfo!, client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -135,6 +145,7 @@ class EthereumWalletService extends EVMChainWalletService { mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); diff --git a/cw_evm/lib/evm_chain_transaction_history.dart b/cw_evm/lib/evm_chain_transaction_history.dart index 2f5c31e82..c4d91783f 100644 --- a/cw_evm/lib/evm_chain_transaction_history.dart +++ b/cw_evm/lib/evm_chain_transaction_history.dart @@ -1,10 +1,10 @@ import 'dart:convert'; import 'dart:core'; import 'dart:developer'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_evm/evm_chain_transaction_info.dart'; -import 'package:cw_evm/file.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; @@ -15,7 +15,8 @@ abstract class EVMChainTransactionHistory = EVMChainTransactionHistoryBase abstract class EVMChainTransactionHistoryBase extends TransactionHistoryBase with Store { - EVMChainTransactionHistoryBase({required this.walletInfo, required String password}) + EVMChainTransactionHistoryBase( + {required this.walletInfo, required String password, required this.encryptionFileUtils}) : _password = password { transactions = ObservableMap(); } @@ -23,6 +24,7 @@ abstract class EVMChainTransactionHistoryBase String _password; final WalletInfo walletInfo; + final EncryptionFileUtils encryptionFileUtils; //! Method to be overridden by all child classes @@ -41,7 +43,7 @@ abstract class EVMChainTransactionHistoryBase final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); String path = '$dirPath/$transactionsHistoryFileNameForWallet'; final data = json.encode({'transactions': transactions}); - await writeData(path: path, password: _password, data: data); + await encryptionFileUtils.write(path: path, password: _password, data: data); } catch (e, s) { log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}'); log(s.toString()); @@ -59,7 +61,7 @@ abstract class EVMChainTransactionHistoryBase final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName(); final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); String path = '$dirPath/$transactionsHistoryFileNameForWallet'; - final content = await read(path: path, password: _password); + final content = await encryptionFileUtils.read(path: path, password: _password); if (content.isEmpty) { return {}; } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 55dcea959..80a366e6f 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -7,6 +7,7 @@ import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -27,7 +28,6 @@ import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_wallet_addresses.dart'; import 'package:cw_evm/evm_ledger_credentials.dart'; -import 'package:cw_evm/file.dart'; import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; @@ -68,6 +68,7 @@ abstract class EVMChainWalletBase String? privateKey, required String password, EVMChainERC20Balance? initialBalance, + required this.encryptionFileUtils, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -83,7 +84,7 @@ abstract class EVMChainWalletBase ), super(walletInfo) { this.walletInfo = walletInfo; - transactionHistory = setUpTransactionHistory(walletInfo, password); + transactionHistory = setUpTransactionHistory(walletInfo, password, encryptionFileUtils); if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) { CakeHive.registerAdapter(Erc20TokenAdapter()); @@ -95,6 +96,7 @@ abstract class EVMChainWalletBase final String? _mnemonic; final String? _hexPrivateKey; final String _password; + final EncryptionFileUtils encryptionFileUtils; late final Box erc20TokensBox; @@ -149,7 +151,11 @@ abstract class EVMChainWalletBase Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath); - EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password); + EVMChainTransactionHistory setUpTransactionHistory( + WalletInfo walletInfo, + String password, + EncryptionFileUtils encryptionFileUtils, + ); //! Common Methods across child classes @@ -510,13 +516,13 @@ abstract class EVMChainWalletBase @override Future save() async { if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { - await saveKeysFile(_password); - saveKeysFile(_password, true); + await saveKeysFile(_password, encryptionFileUtils); + saveKeysFile(_password, encryptionFileUtils, true); } await walletAddresses.updateAddressesInBox(); final path = await makePath(); - await write(path: path, password: _password, data: toJSON()); + await encryptionFileUtils.write(path: path, password: _password, data: toJSON()); await transactionHistory.save(); } @@ -690,4 +696,7 @@ abstract class EVMChainWalletBase bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message))); Web3Client? getWeb3Client() => _client.getWeb3Client(); + + @override + String get password => _password; } diff --git a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart index be763bac7..e8a13cbb9 100644 --- a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart +++ b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart @@ -3,8 +3,8 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class EVMChainNewWalletCredentials extends WalletCredentials { - EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo}) - : super(name: name, walletInfo: walletInfo); + EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) + : super(name: name, walletInfo: walletInfo, password: password); } class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart index 2bbe6bd47..e6bb41b86 100644 --- a/cw_evm/lib/evm_chain_wallet_service.dart +++ b/cw_evm/lib/evm_chain_wallet_service.dart @@ -15,9 +15,10 @@ abstract class EVMChainWalletService extends WalletSer EVMChainRestoreWalletFromSeedCredentials, EVMChainRestoreWalletFromPrivateKey, EVMChainRestoreWalletFromHardware> { - EVMChainWalletService(this.walletInfoSource); + EVMChainWalletService(this.walletInfoSource, this.isDirect); final Box walletInfoSource; + final bool isDirect; @override WalletType getType(); diff --git a/cw_evm/lib/file.dart b/cw_evm/lib/file.dart deleted file mode 100644 index 8fd236ec3..000000000 --- a/cw_evm/lib/file.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:io'; -import 'package:cw_core/key.dart'; -import 'package:encrypt/encrypt.dart' as encrypt; - -Future write( - {required String path, - required String password, - required String data}) async { - final keys = extractKeys(password); - final key = encrypt.Key.fromBase64(keys.first); - final iv = encrypt.IV.fromBase64(keys.last); - final encrypted = await encode(key: key, iv: iv, data: data); - final f = File(path); - f.writeAsStringSync(encrypted); -} - -Future writeData( - {required String path, - required String password, - required String data}) async { - final keys = extractKeys(password); - final key = encrypt.Key.fromBase64(keys.first); - final iv = encrypt.IV.fromBase64(keys.last); - final encrypted = await encode(key: key, iv: iv, data: data); - final f = File(path); - f.writeAsStringSync(encrypted); -} - -Future read({required String path, required String password}) async { - final file = File(path); - - if (!file.existsSync()) { - file.createSync(); - } - - final encrypted = file.readAsStringSync(); - - return decode(password: password, data: encrypted); -} diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index c3f4347c2..b24e375a7 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: hive: ^2.2.3 collection: ^1.17.1 shared_preferences: ^2.0.15 + mobx: ^2.0.7+4 cw_core: path: ../cw_core ledger_flutter: ^1.0.1 diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index e639be4b9..c0ecbca68 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -38,9 +38,10 @@ class HavenWallet = HavenWalletBase with _$HavenWallet; abstract class HavenWalletBase extends WalletBase with Store { - HavenWalletBase({required WalletInfo walletInfo}) + HavenWalletBase({required WalletInfo walletInfo, String? password}) : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)), _isTransactionUpdating = false, + _password = password ?? '', _hasSyncAfterStartup = false, walletAddresses = HavenWalletAddresses(walletInfo), syncStatus = NotConnectedSyncStatus(), @@ -56,6 +57,7 @@ abstract class HavenWalletBase } static const int _autoSaveInterval = 30; + final String _password; @override HavenWalletAddresses walletAddresses; @@ -111,7 +113,7 @@ abstract class HavenWalletBase _onAccountChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); } - + @override Future connectToNode({required Node node}) async { try { @@ -414,4 +416,7 @@ abstract class HavenWalletBase print(e.toString()); } } + + @override + String get password => _password; } diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index c34e164f4..6e840224c 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -113,6 +113,15 @@ packages: url: "https://pub.dev" source: hosted version: "8.4.3" + cake_backup: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" characters: dependency: transitive description: @@ -169,6 +178,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" cw_core: dependency: "direct main" description: @@ -639,6 +664,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/cw_monero/.metadata b/cw_monero/.metadata index 46a2f7f6f..679a0404c 100644 --- a/cw_monero/.metadata +++ b/cw_monero/.metadata @@ -18,6 +18,9 @@ migration: - platform: macos create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: linux + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 # User provided section diff --git a/cw_monero/example/linux/.gitignore b/cw_monero/example/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/cw_monero/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/cw_monero/example/linux/CMakeLists.txt b/cw_monero/example/linux/CMakeLists.txt new file mode 100644 index 000000000..8b2f28252 --- /dev/null +++ b/cw_monero/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "cw_monero_example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.cakewallet.cw_monero") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/cw_monero/example/linux/flutter/CMakeLists.txt b/cw_monero/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..d5bd01648 --- /dev/null +++ b/cw_monero/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/cw_monero/example/linux/flutter/generated_plugin_registrant.cc b/cw_monero/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..1936c88a6 --- /dev/null +++ b/cw_monero/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) cw_monero_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "CwMoneroPlugin"); + cw_monero_plugin_register_with_registrar(cw_monero_registrar); +} diff --git a/cw_monero/example/linux/flutter/generated_plugin_registrant.h b/cw_monero/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/cw_monero/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/cw_monero/example/linux/flutter/generated_plugins.cmake b/cw_monero/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..efcc9a8f9 --- /dev/null +++ b/cw_monero/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + cw_monero +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/cw_monero/example/linux/main.cc b/cw_monero/example/linux/main.cc new file mode 100644 index 000000000..e7c5c5437 --- /dev/null +++ b/cw_monero/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/cw_monero/example/linux/my_application.cc b/cw_monero/example/linux/my_application.cc new file mode 100644 index 000000000..875fc557a --- /dev/null +++ b/cw_monero/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "cw_monero_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "cw_monero_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/cw_monero/example/linux/my_application.h b/cw_monero/example/linux/my_application.h new file mode 100644 index 000000000..72271d5e4 --- /dev/null +++ b/cw_monero/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart index 199896631..7cb95a507 100644 --- a/cw_monero/lib/api/account_list.dart +++ b/cw_monero/lib/api/account_list.dart @@ -34,7 +34,6 @@ List getAllAccount() { // final size = monero.Wallet_numSubaddressAccounts(wptr!); refreshAccounts(); int size = monero.SubaddressAccount_getAll_size(subaddressAccount!); - print("size: $size"); if (size == 0) { monero.Wallet_addSubaddressAccount(wptr!); return getAllAccount(); diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 26c83b06e..14bf92d16 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -7,6 +7,8 @@ import 'package:cw_monero/api/exceptions/wallet_creation_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; +import 'package:cw_monero/api/wallet.dart'; +import 'package:flutter/foundation.dart'; import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:flutter/foundation.dart'; diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 9298f8a49..31e09ca2d 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -3,6 +3,8 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/account.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/monero_amount_format.dart'; @@ -50,7 +52,8 @@ abstract class MoneroWalletBase extends WalletBase with Store { MoneroWalletBase( {required WalletInfo walletInfo, - required Box unspentCoinsInfo}) + required Box unspentCoinsInfo, + required String password}) : balance = ObservableMap.of({ CryptoCurrency.xmr: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), @@ -59,6 +62,7 @@ abstract class MoneroWalletBase extends WalletBase monero_wallet.getSeed(); String seedLegacy(String? language) => monero_wallet.getSeedLegacy(language); + @override + String get password => _password; + @override MoneroWalletKeys get keys => MoneroWalletKeys( privateSpendKey: monero_wallet.getSecretSpendKey(), @@ -127,6 +134,7 @@ abstract class MoneroWalletBase extends WalletBase unspentCoins; + String _password; Future init() async { await walletAddresses.init(); diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 3588ebb78..d771d1815 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -93,7 +93,9 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager.createWallet( path: path, password: credentials.password!, language: credentials.language); final wallet = MoneroWallet( - walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -126,10 +128,14 @@ class MoneroWalletService extends WalletService< await repairOldAndroidWallet(name); } - await monero_wallet_manager.openWalletAsync({'path': path, 'password': password}); - final walletInfo = walletInfoSource.values - .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); - wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + await monero_wallet_manager + .openWalletAsync({'path': path, 'password': password}); + final walletInfo = walletInfoSource.values.firstWhere( + (info) => info.id == WalletBase.idFor(name, getType())); + final wallet = MoneroWallet( + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + password: password); final isValid = wallet.walletAddresses.validate(); if (!isValid) { @@ -162,15 +168,22 @@ class MoneroWalletService extends WalletService< final bool invalidSignature = e.toString().contains('invalid signature') || (e is WalletOpeningException && e.message.contains('invalid signature')); + final bool invalidPassword = e.toString().contains('invalid password') || + (e is WalletOpeningException && e.message.contains('invalid password')); + if (!isBadAlloc && !doesNotCorrespond && !isMissingCacheFilesIOS && !isMissingCacheFilesAndroid && !invalidSignature && + !invalidPassword && wallet != null && wallet.onError != null) { wallet.onError!(FlutterErrorDetails(exception: e, stack: s)); } + if (invalidPassword) { + rethrow; + } await restoreOrResetWalletFiles(name); return openWallet(name, password); @@ -206,11 +219,15 @@ class MoneroWalletService extends WalletService< } @override - Future rename(String currentName, String password, String newName) async { - final currentWalletInfo = walletInfoSource.values - .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); - final currentWallet = - MoneroWallet(walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + Future rename( + String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values.firstWhere( + (info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = MoneroWallet( + walletInfo: currentWalletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + password: password, + ); await currentWallet.renameWalletFiles(newName); @@ -235,7 +252,9 @@ class MoneroWalletService extends WalletService< viewKey: credentials.viewKey, spendKey: credentials.spendKey); final wallet = MoneroWallet( - walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -268,7 +287,9 @@ class MoneroWalletService extends WalletService< seed: credentials.mnemonic, restoreHeight: credentials.height!); final wallet = MoneroWallet( - walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -315,7 +336,11 @@ class MoneroWalletService extends WalletService< restoreHeight: height, spendKey: spendKey); - final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + final wallet = MoneroWallet( + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + password: password, + ); await wallet.init(); return wallet; @@ -364,7 +389,11 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager.openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); - final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + final wallet = MoneroWallet( + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + password: password, + ); return wallet.seed; } catch (_) { // if the file couldn't be opened or read diff --git a/cw_monero/linux/CMakeLists.txt b/cw_monero/linux/CMakeLists.txt new file mode 100644 index 000000000..ba685269d --- /dev/null +++ b/cw_monero/linux/CMakeLists.txt @@ -0,0 +1,270 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "cw_monero") +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed. +set(PLUGIN_NAME "cw_monero_plugin") + +# Define the plugin library target. Its name must not be changed (see comment +# on PLUGIN_NAME above). +# +# Any new source files that you add to the plugin should be added here. +add_library(${PLUGIN_NAME} SHARED + "cw_monero_plugin.cc" + "../ios/Classes/monero_api.cpp" +) + +# Apply a standard set of build settings that are configured in the +# application-level CMakeLists.txt. This can be removed for plugins that want +# full control over build settings. +apply_standard_settings(${PLUGIN_NAME}) + +# Symbols are hidden by default to reduce the chance of accidental conflicts +# between plugins. This should not be removed; any symbols that should be +# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) + +# Source include directories and library dependencies. Add any plugin-specific +# dependencies here. +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) +target_link_libraries(${PLUGIN_NAME} PUBLIC cw_monero) +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(cw_monero_bundled_libraries + "" + PARENT_SCOPE +) + +add_library( cw_monero + STATIC + ../ios/Classes/monero_api.cpp) + +set(EXTERNAL_LIBS_DIR ${CMAKE_SOURCE_DIR}/../cw_shared_external/ios/External/linux) + +############ +# libsodium +############ + +add_library(sodium STATIC IMPORTED) +set_target_properties(sodium PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libsodium.a) + +############ +# OpenSSL +############ + +add_library(crypto STATIC IMPORTED) +set_target_properties(crypto PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libcrypto.a) + +add_library(ssl STATIC IMPORTED) +set_target_properties(ssl PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libssl.a) + +############ +# Boost +############ + +add_library(boost_chrono STATIC IMPORTED) +set_target_properties(boost_chrono PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libboost_chrono.a) + +add_library(boost_date_time STATIC IMPORTED) +set_target_properties(boost_date_time PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libboost_date_time.a) + +add_library(boost_filesystem STATIC IMPORTED) +set_target_properties(boost_filesystem PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libboost_filesystem.a) + +add_library(boost_program_options STATIC IMPORTED) +set_target_properties(boost_program_options PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libboost_program_options.a) + +add_library(boost_regex STATIC IMPORTED) +set_target_properties(boost_regex PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libboost_regex.a) + +add_library(boost_serialization STATIC IMPORTED) +set_target_properties(boost_serialization PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libboost_serialization.a) + +add_library(boost_system STATIC IMPORTED) +set_target_properties(boost_system PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libboost_system.a) + +add_library(boost_thread STATIC IMPORTED) +set_target_properties(boost_thread PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libboost_thread.a) + +add_library(boost_wserialization STATIC IMPORTED) +set_target_properties(boost_wserialization PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libboost_wserialization.a) + +############# +# Monero +############# + + +add_library(wallet STATIC IMPORTED) +set_target_properties(wallet PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libwallet.a) + +add_library(wallet_api STATIC IMPORTED) +set_target_properties(wallet_api PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libwallet_api.a) + +add_library(cryptonote_core STATIC IMPORTED) +set_target_properties(cryptonote_core PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libcryptonote_core.a) + +add_library(cryptonote_basic STATIC IMPORTED) +set_target_properties(cryptonote_basic PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libcryptonote_basic.a) + +add_library(cryptonote_format_utils_basic STATIC IMPORTED) +set_target_properties(cryptonote_format_utils_basic PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libcryptonote_format_utils_basic.a) + +add_library(mnemonics STATIC IMPORTED) +set_target_properties(mnemonics PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libmnemonics.a) + +add_library(common STATIC IMPORTED) +set_target_properties(common PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libcommon.a) + +add_library(cncrypto STATIC IMPORTED) +set_target_properties(cncrypto PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libcncrypto.a) + +add_library(ringct STATIC IMPORTED) +set_target_properties(ringct PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libringct.a) + +add_library(ringct_basic STATIC IMPORTED) +set_target_properties(ringct_basic PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libringct_basic.a) + +add_library(blockchain_db STATIC IMPORTED) +set_target_properties(blockchain_db PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libblockchain_db.a) + +add_library(lmdb STATIC IMPORTED) +set_target_properties(lmdb PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/liblmdb.a) + +add_library(easylogging STATIC IMPORTED) +set_target_properties(easylogging PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libeasylogging.a) + +add_library(epee STATIC IMPORTED) +set_target_properties(epee PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libepee.a) + +add_library(blocks STATIC IMPORTED) +set_target_properties(blocks PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libblocks.a) + +add_library(checkpoints STATIC IMPORTED) +set_target_properties(checkpoints PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libcheckpoints.a) + +add_library(device STATIC IMPORTED) +set_target_properties(device PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libdevice.a) + +add_library(device_trezor STATIC IMPORTED) +set_target_properties(device_trezor PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libdevice_trezor.a) + +add_library(multisig STATIC IMPORTED) +set_target_properties(multisig PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libmultisig.a) + +add_library(version STATIC IMPORTED) +set_target_properties(version PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libversion.a) + +add_library(net STATIC IMPORTED) +set_target_properties(net PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libnet.a) + +add_library(hardforks STATIC IMPORTED) +set_target_properties(hardforks PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libhardforks.a) + +add_library(randomx STATIC IMPORTED) +set_target_properties(randomx PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/librandomx.a) + +add_library(rpc_base STATIC IMPORTED) +set_target_properties(rpc_base PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/librpc_base.a) + +add_library(wallet-crypto STATIC IMPORTED) +set_target_properties(wallet-crypto PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/monero/libwallet-crypto.a) + +add_library(unbound STATIC IMPORTED) +set_target_properties(unbound PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/lib/libunbound.a) + +include_directories( ${EXTERNAL_LIBS_DIR}/include ) + +target_link_libraries( cw_monero + + wallet_api + wallet + cryptonote_core + cryptonote_basic + cryptonote_format_utils_basic + mnemonics + ringct + ringct_basic + net + common + cncrypto + blockchain_db + lmdb + easylogging + unbound + epee + blocks + checkpoints + device + device_trezor + multisig + version + randomx + hardforks + rpc_base + wallet-crypto + + boost_chrono + boost_date_time + boost_filesystem + boost_program_options + boost_regex + boost_serialization + boost_system + boost_thread + boost_wserialization + + ssl + crypto + + sodium + ) diff --git a/cw_monero/linux/cw_monero_plugin.cc b/cw_monero/linux/cw_monero_plugin.cc new file mode 100644 index 000000000..ca8524c9e --- /dev/null +++ b/cw_monero/linux/cw_monero_plugin.cc @@ -0,0 +1,70 @@ +#include "include/cw_monero/cw_monero_plugin.h" + +#include +#include +#include + +#include + +#define CW_MONERO_PLUGIN(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), cw_monero_plugin_get_type(), \ + CwMoneroPlugin)) + +struct _CwMoneroPlugin { + GObject parent_instance; +}; + +G_DEFINE_TYPE(CwMoneroPlugin, cw_monero_plugin, g_object_get_type()) + +// Called when a method call is received from Flutter. +static void cw_monero_plugin_handle_method_call( + CwMoneroPlugin* self, + FlMethodCall* method_call) { + g_autoptr(FlMethodResponse) response = nullptr; + + const gchar* method = fl_method_call_get_name(method_call); + + if (strcmp(method, "getPlatformVersion") == 0) { + struct utsname uname_data = {}; + uname(&uname_data); + g_autofree gchar *version = g_strdup_printf("Linux %s", uname_data.version); + g_autoptr(FlValue) result = fl_value_new_string(version); + response = FL_METHOD_RESPONSE(fl_method_success_response_new(result)); + } else { + response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } + + fl_method_call_respond(method_call, response, nullptr); +} + +static void cw_monero_plugin_dispose(GObject* object) { + G_OBJECT_CLASS(cw_monero_plugin_parent_class)->dispose(object); +} + +static void cw_monero_plugin_class_init(CwMoneroPluginClass* klass) { + G_OBJECT_CLASS(klass)->dispose = cw_monero_plugin_dispose; +} + +static void cw_monero_plugin_init(CwMoneroPlugin* self) {} + +static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, + gpointer user_data) { + CwMoneroPlugin* plugin = CW_MONERO_PLUGIN(user_data); + cw_monero_plugin_handle_method_call(plugin, method_call); +} + +void cw_monero_plugin_register_with_registrar(FlPluginRegistrar* registrar) { + CwMoneroPlugin* plugin = CW_MONERO_PLUGIN( + g_object_new(cw_monero_plugin_get_type(), nullptr)); + + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + g_autoptr(FlMethodChannel) channel = + fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), + "cw_monero", + FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(channel, method_call_cb, + g_object_ref(plugin), + g_object_unref); + + g_object_unref(plugin); +} diff --git a/cw_monero/linux/flutter/generated_plugin_registrant.cc b/cw_monero/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..e71a16d23 --- /dev/null +++ b/cw_monero/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/cw_monero/linux/flutter/generated_plugin_registrant.h b/cw_monero/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/cw_monero/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/cw_monero/linux/flutter/generated_plugins.cmake b/cw_monero/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..2e1de87a7 --- /dev/null +++ b/cw_monero/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/cw_monero/linux/include/cw_monero/cw_monero_plugin.h b/cw_monero/linux/include/cw_monero/cw_monero_plugin.h new file mode 100644 index 000000000..387903ff6 --- /dev/null +++ b/cw_monero/linux/include/cw_monero/cw_monero_plugin.h @@ -0,0 +1,26 @@ +#ifndef FLUTTER_PLUGIN_CW_MONERO_PLUGIN_H_ +#define FLUTTER_PLUGIN_CW_MONERO_PLUGIN_H_ + +#include + +G_BEGIN_DECLS + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +typedef struct _CwMoneroPlugin CwMoneroPlugin; +typedef struct { + GObjectClass parent_class; +} CwMoneroPluginClass; + +FLUTTER_PLUGIN_EXPORT GType cw_monero_plugin_get_type(); + +FLUTTER_PLUGIN_EXPORT void cw_monero_plugin_register_with_registrar( + FlPluginRegistrar* registrar); + +G_END_DECLS + +#endif // FLUTTER_PLUGIN_CW_MONERO_PLUGIN_H_ diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 38299b2dc..07c3b8876 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -113,6 +113,15 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.2" + cake_backup: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" characters: dependency: transitive description: @@ -169,6 +178,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" cw_core: dependency: "direct main" description: @@ -342,10 +367,10 @@ packages: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -696,6 +721,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/cw_nano/lib/file.dart b/cw_nano/lib/file.dart deleted file mode 100644 index 8fd236ec3..000000000 --- a/cw_nano/lib/file.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:io'; -import 'package:cw_core/key.dart'; -import 'package:encrypt/encrypt.dart' as encrypt; - -Future write( - {required String path, - required String password, - required String data}) async { - final keys = extractKeys(password); - final key = encrypt.Key.fromBase64(keys.first); - final iv = encrypt.IV.fromBase64(keys.last); - final encrypted = await encode(key: key, iv: iv, data: data); - final f = File(path); - f.writeAsStringSync(encrypted); -} - -Future writeData( - {required String path, - required String password, - required String data}) async { - final keys = extractKeys(password); - final key = encrypt.Key.fromBase64(keys.first); - final iv = encrypt.IV.fromBase64(keys.last); - final encrypted = await encode(key: key, iv: iv, data: data); - final f = File(path); - f.writeAsStringSync(encrypted); -} - -Future read({required String path, required String password}) async { - final file = File(path); - - if (!file.existsSync()) { - file.createSync(); - } - - final encrypted = file.readAsStringSync(); - - return decode(password: password, data: encrypted); -} diff --git a/cw_nano/lib/nano_transaction_history.dart b/cw_nano/lib/nano_transaction_history.dart index dadd353c4..44d64f7d4 100644 --- a/cw_nano/lib/nano_transaction_history.dart +++ b/cw_nano/lib/nano_transaction_history.dart @@ -2,24 +2,29 @@ import 'dart:convert'; import 'dart:core'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_nano/file.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_nano/nano_transaction_info.dart'; part 'nano_transaction_history.g.dart'; + const transactionsHistoryFileName = 'transactions.json'; class NanoTransactionHistory = NanoTransactionHistoryBase with _$NanoTransactionHistory; -abstract class NanoTransactionHistoryBase - extends TransactionHistoryBase with Store { - NanoTransactionHistoryBase({required this.walletInfo, required String password}) - : _password = password { +abstract class NanoTransactionHistoryBase extends TransactionHistoryBase + with Store { + NanoTransactionHistoryBase({ + required this.walletInfo, + required String password, + required this.encryptionFileUtils, + }) : _password = password { transactions = ObservableMap(); } final WalletInfo walletInfo; + final EncryptionFileUtils encryptionFileUtils; String _password; Future init() async => await _load(); @@ -30,7 +35,7 @@ abstract class NanoTransactionHistoryBase final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final path = '$dirPath/$transactionsHistoryFileName'; final data = json.encode({'transactions': transactions}); - await writeData(path: path, password: _password, data: data); + await encryptionFileUtils.write(path: path, password: _password, data: data); } catch (e) { print('Error while save nano transaction history: ${e.toString()}'); } @@ -46,7 +51,10 @@ abstract class NanoTransactionHistoryBase Future> _read() async { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final path = '$dirPath/$transactionsHistoryFileName'; - final content = await read(path: path, password: _password); + final content = await encryptionFileUtils.read(path: path, password: _password); + if (content.isEmpty) { + return {}; + } return json.decode(content) as Map; } diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 55e01d10b..cba8d09a0 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:bip39/bip39.dart' as bip39; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/n2_node.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/nano_account_info_response.dart'; @@ -17,7 +18,6 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; -import 'package:cw_nano/file.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_client.dart'; import 'package:cw_nano/nano_transaction_credentials.dart'; @@ -42,11 +42,13 @@ abstract class NanoWalletBase required String mnemonic, required String password, NanoBalance? initialBalance, + required EncryptionFileUtils encryptionFileUtils, }) : syncStatus = NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, _derivationType = walletInfo.derivationInfo!.derivationType!, _isTransactionUpdating = false, + _encryptionFileUtils = encryptionFileUtils, _client = NanoClient(), walletAddresses = NanoWalletAddresses(walletInfo), balance = ObservableMap.of({ @@ -55,7 +57,11 @@ abstract class NanoWalletBase }), super(walletInfo) { this.walletInfo = walletInfo; - transactionHistory = NanoTransactionHistory(walletInfo: walletInfo, password: password); + transactionHistory = NanoTransactionHistory( + walletInfo: walletInfo, + password: password, + encryptionFileUtils: encryptionFileUtils, + ); if (!CakeHive.isAdapterRegistered(NanoAccount.typeId)) { CakeHive.registerAdapter(NanoAccountAdapter()); } @@ -65,6 +71,8 @@ abstract class NanoWalletBase final String _password; DerivationType _derivationType; + final EncryptionFileUtils _encryptionFileUtils; + String? _privateKey; String? _publicAddress; String? _hexSeed; @@ -89,6 +97,9 @@ abstract class NanoWalletBase @observable late ObservableMap balance; + @override + String get password => _password; + static const int POLL_INTERVAL_SECONDS = 10; // initialize the different forms of private / public key we'll need: @@ -308,13 +319,13 @@ abstract class NanoWalletBase @override Future save() async { if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { - await saveKeysFile(_password); - saveKeysFile(_password, true); + await saveKeysFile(_password, _encryptionFileUtils); + saveKeysFile(_password, _encryptionFileUtils, true); } await walletAddresses.updateAddressesInBox(); final path = await makePath(); - await write(path: path, password: _password, data: toJSON()); + await _encryptionFileUtils.write(path: path, password: _password, data: toJSON()); await transactionHistory.save(); } @@ -373,13 +384,14 @@ abstract class NanoWalletBase required String name, required String password, required WalletInfo walletInfo, + required EncryptionFileUtils encryptionFileUtils, }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); Map? data = null; try { - final jsonSource = await read(path: path, password: password); + final jsonSource = await encryptionFileUtils.read(path: path, password: password); data = json.decode(jsonSource) as Map; } catch (e) { @@ -400,7 +412,12 @@ abstract class NanoWalletBase keysData = WalletKeysData( mnemonic: isHexSeed ? null : mnemonic, altMnemonic: isHexSeed ? mnemonic : null); } else { - keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); } DerivationType derivationType = DerivationType.nano; @@ -416,6 +433,7 @@ abstract class NanoWalletBase password: password, mnemonic: keysData.mnemonic!, initialBalance: balance, + encryptionFileUtils: encryptionFileUtils, ); // init() should always be run after this! } diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index 755598705..ac3d6581a 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; @@ -15,9 +16,10 @@ import 'package:nanoutil/nanoutil.dart'; class NanoWalletService extends WalletService { - NanoWalletService(this.walletInfoSource); + NanoWalletService(this.walletInfoSource, this.isDirect); final Box walletInfoSource; + final bool isDirect; static bool walletFilesExist(String path) => !File(path).existsSync() && !File('$path.keys').existsSync(); @@ -38,6 +40,7 @@ class NanoWalletService extends WalletService.from(nm.NanoMnemomics.WORDLIST)..shuffle()).take(24).join(' '); - final currentWallet = - NanoWallet(walletInfo: currentWalletInfo, password: password, mnemonic: randomWords); + final currentWallet = NanoWallet( + walletInfo: currentWalletInfo, + password: password, + mnemonic: randomWords, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); @@ -103,6 +110,7 @@ class NanoWalletService extends WalletService open( - {required String name, required String password, required WalletInfo walletInfo}) async { + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + required EncryptionFileUtils encryptionFileUtils, + }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); Map? data; try { - final jsonSource = await read(path: path, password: password); + final jsonSource = await encryptionFileUtils.read(path: path, password: password); data = json.decode(jsonSource) as Map; } catch (e) { @@ -121,7 +131,12 @@ class PolygonWallet extends EVMChainWallet { keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); } else { - keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); } return PolygonWallet( @@ -131,6 +146,7 @@ class PolygonWallet extends EVMChainWallet { privateKey: keysData.privateKey, initialBalance: balance, client: PolygonClient(), + encryptionFileUtils: encryptionFileUtils, ); } } diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index 14baffc44..4efc312f7 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -1,4 +1,5 @@ import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -10,7 +11,7 @@ import 'package:cw_polygon/polygon_wallet.dart'; class PolygonWalletService extends EVMChainWalletService { PolygonWalletService( - super.walletInfoSource, { + super.walletInfoSource, super.isDirect, { required this.client, }); @@ -30,6 +31,7 @@ class PolygonWalletService extends EVMChainWalletService { mnemonic: mnemonic, password: credentials.password!, client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -48,6 +50,7 @@ class PolygonWalletService extends EVMChainWalletService { name: name, password: password, walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -61,6 +64,7 @@ class PolygonWalletService extends EVMChainWalletService { name: name, password: password, walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -77,6 +81,7 @@ class PolygonWalletService extends EVMChainWalletService { privateKey: credentials.privateKey, walletInfo: credentials.walletInfo!, client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -99,6 +104,7 @@ class PolygonWalletService extends EVMChainWalletService { walletInfo: credentials.walletInfo!, password: credentials.password!, client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -120,6 +126,7 @@ class PolygonWalletService extends EVMChainWalletService { mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -134,7 +141,11 @@ class PolygonWalletService extends EVMChainWalletService { final currentWalletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); final currentWallet = await PolygonWallet.open( - password: password, name: currentName, walletInfo: currentWalletInfo); + password: password, + name: currentName, + walletInfo: currentWalletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); diff --git a/cw_solana/lib/file.dart b/cw_solana/lib/file.dart deleted file mode 100644 index 8fd236ec3..000000000 --- a/cw_solana/lib/file.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:io'; -import 'package:cw_core/key.dart'; -import 'package:encrypt/encrypt.dart' as encrypt; - -Future write( - {required String path, - required String password, - required String data}) async { - final keys = extractKeys(password); - final key = encrypt.Key.fromBase64(keys.first); - final iv = encrypt.IV.fromBase64(keys.last); - final encrypted = await encode(key: key, iv: iv, data: data); - final f = File(path); - f.writeAsStringSync(encrypted); -} - -Future writeData( - {required String path, - required String password, - required String data}) async { - final keys = extractKeys(password); - final key = encrypt.Key.fromBase64(keys.first); - final iv = encrypt.IV.fromBase64(keys.last); - final encrypted = await encode(key: key, iv: iv, data: data); - final f = File(path); - f.writeAsStringSync(encrypted); -} - -Future read({required String path, required String password}) async { - final file = File(path); - - if (!file.existsSync()) { - file.createSync(); - } - - final encrypted = file.readAsStringSync(); - - return decode(password: password, data: encrypted); -} diff --git a/cw_solana/lib/solana_transaction_history.dart b/cw_solana/lib/solana_transaction_history.dart index c03de19ad..77f93b9ee 100644 --- a/cw_solana/lib/solana_transaction_history.dart +++ b/cw_solana/lib/solana_transaction_history.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'dart:core'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_solana/file.dart'; import 'package:cw_solana/solana_transaction_info.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; @@ -15,12 +15,14 @@ class SolanaTransactionHistory = SolanaTransactionHistoryBase with _$SolanaTrans abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase with Store { - SolanaTransactionHistoryBase({required this.walletInfo, required String password}) + SolanaTransactionHistoryBase( + {required this.walletInfo, required String password, required this.encryptionFileUtils}) : _password = password { transactions = ObservableMap(); } final WalletInfo walletInfo; + final EncryptionFileUtils encryptionFileUtils; String _password; Future init() async => await _load(); @@ -32,7 +34,7 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase MapEntry(key, value.toJson())); final data = json.encode({'transactions': transactionMaps}); - await writeData(path: path, password: _password, data: data); + await encryptionFileUtils.write(path: path, password: _password, data: data); } catch (e, s) { print('Error while saving solana transaction history: ${e.toString()}'); print(s); @@ -49,7 +51,7 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase> _read() async { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final path = '$dirPath/$transactionsHistoryFileName'; - final content = await read(path: path, password: _password); + final content = await encryptionFileUtils.read(path: path, password: _password); if (content.isEmpty) { return {}; } diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 2b30a204c..66b8bca42 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -15,7 +16,6 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_solana/default_spl_tokens.dart'; -import 'package:cw_solana/file.dart'; import 'package:cw_solana/solana_balance.dart'; import 'package:cw_solana/solana_client.dart'; import 'package:cw_solana/solana_exceptions.dart'; @@ -46,6 +46,7 @@ abstract class SolanaWalletBase String? privateKey, required String password, SolanaBalance? initialBalance, + required this.encryptionFileUtils, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -56,7 +57,11 @@ abstract class SolanaWalletBase {CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble())}), super(walletInfo) { this.walletInfo = walletInfo; - transactionHistory = SolanaTransactionHistory(walletInfo: walletInfo, password: password); + transactionHistory = SolanaTransactionHistory( + walletInfo: walletInfo, + password: password, + encryptionFileUtils: encryptionFileUtils, + ); if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) { CakeHive.registerAdapter(SPLTokenAdapter()); @@ -68,6 +73,7 @@ abstract class SolanaWalletBase final String _password; final String? _mnemonic; final String? _hexPrivateKey; + final EncryptionFileUtils encryptionFileUtils; // The Solana WalletPair Ed25519HDKeyPair? _walletKeyPair; @@ -77,7 +83,7 @@ abstract class SolanaWalletBase // To access the privateKey bytes. Ed25519HDKeyPairData? _keyPairData; - late SolanaWalletClient _client; + late final SolanaWalletClient _client; @observable double? estimatedFee; @@ -97,7 +103,7 @@ abstract class SolanaWalletBase @observable late ObservableMap balance; - Completer _sharedPrefs = Completer(); + final Completer _sharedPrefs = Completer(); @override Ed25519HDKeyPairData get keys { @@ -343,13 +349,13 @@ abstract class SolanaWalletBase @override Future save() async { if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { - await saveKeysFile(_password); - saveKeysFile(_password, true); + await saveKeysFile(_password, encryptionFileUtils); + saveKeysFile(_password, encryptionFileUtils, true); } await walletAddresses.updateAddressesInBox(); final path = await makePath(); - await write(path: path, password: _password, data: toJSON()); + await encryptionFileUtils.write(path: path, password: _password, data: toJSON()); await transactionHistory.save(); } @@ -382,13 +388,14 @@ abstract class SolanaWalletBase required String name, required String password, required WalletInfo walletInfo, + required EncryptionFileUtils encryptionFileUtils, }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); Map? data; try { - final jsonSource = await read(path: path, password: password); + final jsonSource = await encryptionFileUtils.read(path: path, password: password); data = json.decode(jsonSource) as Map; } catch (e) { @@ -405,7 +412,12 @@ abstract class SolanaWalletBase keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); } else { - keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); } return SolanaWallet( @@ -414,6 +426,7 @@ abstract class SolanaWalletBase mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, initialBalance: balance, + encryptionFileUtils: encryptionFileUtils, ); } @@ -572,4 +585,7 @@ abstract class SolanaWalletBase } SolanaClient? get solanaClient => _client.getSolanaClient; + + @override + String get password => _password; } diff --git a/cw_solana/lib/solana_wallet_creation_credentials.dart b/cw_solana/lib/solana_wallet_creation_credentials.dart index 881c30abd..5b4fa1774 100644 --- a/cw_solana/lib/solana_wallet_creation_credentials.dart +++ b/cw_solana/lib/solana_wallet_creation_credentials.dart @@ -2,8 +2,8 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class SolanaNewWalletCredentials extends WalletCredentials { - SolanaNewWalletCredentials({required String name, WalletInfo? walletInfo}) - : super(name: name, walletInfo: walletInfo); + SolanaNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) + : super(name: name, walletInfo: walletInfo, password: password); } class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index 4afb2f7f4..7461be33b 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:bip39/bip39.dart' as bip39; import 'package:collection/collection.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_history.dart'; @@ -17,9 +18,10 @@ import 'package:hive/hive.dart'; class SolanaWalletService extends WalletService { - SolanaWalletService(this.walletInfoSource); + SolanaWalletService(this.walletInfoSource, this.isDirect); final Box walletInfoSource; + final bool isDirect; @override Future create(SolanaNewWalletCredentials credentials, {bool? isTestnet}) async { @@ -31,6 +33,7 @@ class SolanaWalletService extends WalletService info.id == WalletBase.idFor(currentName, getType())); final currentWallet = await SolanaWalletBase.open( - password: password, name: currentName, walletInfo: currentWalletInfo); + password: password, + name: currentName, + walletInfo: currentWalletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); diff --git a/cw_tron/lib/file.dart b/cw_tron/lib/file.dart deleted file mode 100644 index 8fd236ec3..000000000 --- a/cw_tron/lib/file.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:io'; -import 'package:cw_core/key.dart'; -import 'package:encrypt/encrypt.dart' as encrypt; - -Future write( - {required String path, - required String password, - required String data}) async { - final keys = extractKeys(password); - final key = encrypt.Key.fromBase64(keys.first); - final iv = encrypt.IV.fromBase64(keys.last); - final encrypted = await encode(key: key, iv: iv, data: data); - final f = File(path); - f.writeAsStringSync(encrypted); -} - -Future writeData( - {required String path, - required String password, - required String data}) async { - final keys = extractKeys(password); - final key = encrypt.Key.fromBase64(keys.first); - final iv = encrypt.IV.fromBase64(keys.last); - final encrypted = await encode(key: key, iv: iv, data: data); - final f = File(path); - f.writeAsStringSync(encrypted); -} - -Future read({required String path, required String password}) async { - final file = File(path); - - if (!file.existsSync()) { - file.createSync(); - } - - final encrypted = file.readAsStringSync(); - - return decode(password: password, data: encrypted); -} diff --git a/cw_tron/lib/tron_transaction_history.dart b/cw_tron/lib/tron_transaction_history.dart index 7d7274226..9d226c09c 100644 --- a/cw_tron/lib/tron_transaction_history.dart +++ b/cw_tron/lib/tron_transaction_history.dart @@ -3,7 +3,7 @@ import 'dart:core'; import 'dart:developer'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_evm/file.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_tron/tron_transaction_info.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; @@ -14,7 +14,8 @@ class TronTransactionHistory = TronTransactionHistoryBase with _$TronTransaction abstract class TronTransactionHistoryBase extends TransactionHistoryBase with Store { - TronTransactionHistoryBase({required this.walletInfo, required String password}) + TronTransactionHistoryBase( + {required this.walletInfo, required String password, required this.encryptionFileUtils}) : _password = password { transactions = ObservableMap(); } @@ -22,6 +23,7 @@ abstract class TronTransactionHistoryBase extends TransactionHistoryBase init() async => await _load(); @@ -33,7 +35,7 @@ abstract class TronTransactionHistoryBase extends TransactionHistoryBase MapEntry(key, value.toJson())); final data = json.encode({'transactions': transactionMaps}); - await writeData(path: path, password: _password, data: data); + await encryptionFileUtils.write(path: path, password: _password, data: data); } catch (e, s) { log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}'); log(s.toString()); @@ -51,7 +53,7 @@ abstract class TronTransactionHistoryBase extends TransactionHistoryBase tronTokensBox; @@ -125,13 +128,14 @@ abstract class TronWalletBase required String name, required String password, required WalletInfo walletInfo, + required EncryptionFileUtils encryptionFileUtils, }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); Map? data; try { - final jsonSource = await read(path: path, password: password); + final jsonSource = await encryptionFileUtils.read(path: path, password: password); data = json.decode(jsonSource) as Map; } catch (e) { @@ -148,7 +152,12 @@ abstract class TronWalletBase keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); } else { - keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); } return TronWallet( @@ -157,6 +166,7 @@ abstract class TronWalletBase mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, initialBalance: balance, + encryptionFileUtils: encryptionFileUtils, ); } @@ -430,13 +440,13 @@ abstract class TronWalletBase @override Future save() async { if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { - await saveKeysFile(_password); - saveKeysFile(_password, true); + await saveKeysFile(_password, encryptionFileUtils); + saveKeysFile(_password, encryptionFileUtils, true); } await walletAddresses.updateAddressesInBox(); final path = await makePath(); - await write(path: path, password: _password, data: toJSON()); + await encryptionFileUtils.write(path: path, password: _password, data: toJSON()); await transactionHistory.save(); } @@ -584,4 +594,7 @@ abstract class TronWalletBase _transactionsUpdateTimer?.cancel(); } } + + @override + String get password => _password; } diff --git a/cw_tron/lib/tron_wallet_creation_credentials.dart b/cw_tron/lib/tron_wallet_creation_credentials.dart index dc4f389aa..ed5e1c164 100644 --- a/cw_tron/lib/tron_wallet_creation_credentials.dart +++ b/cw_tron/lib/tron_wallet_creation_credentials.dart @@ -2,8 +2,8 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class TronNewWalletCredentials extends WalletCredentials { - TronNewWalletCredentials({required String name, WalletInfo? walletInfo}) - : super(name: name, walletInfo: walletInfo); + TronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) + : super(name: name, walletInfo: walletInfo, password: password); } class TronRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart index ba217a265..dacef439a 100644 --- a/cw_tron/lib/tron_wallet_service.dart +++ b/cw_tron/lib/tron_wallet_service.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:bip39/bip39.dart' as bip39; import 'package:collection/collection.dart'; import 'package:cw_core/balance.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; @@ -21,11 +22,12 @@ class TronWalletService extends WalletService< TronRestoreWalletFromSeedCredentials, TronRestoreWalletFromPrivateKey, TronNewWalletCredentials> { - TronWalletService(this.walletInfoSource, {required this.client}); + TronWalletService(this.walletInfoSource, {required this.client, required this.isDirect}); late TronClient client; final Box walletInfoSource; + final bool isDirect; @override WalletType getType() => WalletType.tron; @@ -43,6 +45,7 @@ class TronWalletService extends WalletService< walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -62,6 +65,7 @@ class TronWalletService extends WalletService< name: name, password: password, walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -75,6 +79,7 @@ class TronWalletService extends WalletService< name: name, password: password, walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -92,6 +97,7 @@ class TronWalletService extends WalletService< password: credentials.password!, privateKey: credentials.privateKey, walletInfo: credentials.walletInfo!, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -114,6 +120,7 @@ class TronWalletService extends WalletService< password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -128,7 +135,11 @@ class TronWalletService extends WalletService< final currentWalletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); final currentWallet = await TronWalletBase.open( - password: password, name: currentName, walletInfo: currentWalletInfo); + password: password, + name: currentName, + walletInfo: currentWalletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index e02c0ec2e..85f5e4b2f 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -50,7 +50,7 @@ abstract class WowneroWalletBase extends WalletBase with Store { WowneroWalletBase( - {required WalletInfo walletInfo, required Box unspentCoinsInfo}) + {required WalletInfo walletInfo, required Box unspentCoinsInfo, required String password}) : balance = ObservableMap.of({ CryptoCurrency.wow: WowneroBalance( fullBalance: wownero_wallet.getFullBalance(accountIndex: 0), @@ -58,6 +58,7 @@ abstract class WowneroWalletBase }), _isTransactionUpdating = false, _hasSyncAfterStartup = false, + _password = password, isEnabledAutoGenerateSubaddress = false, syncStatus = NotConnectedSyncStatus(), unspentCoins = [], @@ -109,6 +110,10 @@ abstract class WowneroWalletBase String seedLegacy(String? language) => wownero_wallet.getSeedLegacy(language); + String get password => _password; + + String _password; + @override MoneroWalletKeys get keys => MoneroWalletKeys( privateSpendKey: wownero_wallet.getSecretSpendKey(), diff --git a/cw_wownero/lib/wownero_wallet_service.dart b/cw_wownero/lib/wownero_wallet_service.dart index 13cab8f61..286bfccd0 100644 --- a/cw_wownero/lib/wownero_wallet_service.dart +++ b/cw_wownero/lib/wownero_wallet_service.dart @@ -93,7 +93,7 @@ class WowneroWalletService extends WalletService< await wownero_wallet_manager.createWallet( path: path, password: credentials.password!, language: credentials.language); final wallet = WowneroWallet( - walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); await wallet.init(); return wallet; @@ -129,7 +129,7 @@ class WowneroWalletService extends WalletService< await wownero_wallet_manager.openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); - wallet = WowneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + wallet = WowneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password); final isValid = wallet.walletAddresses.validate(); if (!isValid) { @@ -210,7 +210,7 @@ class WowneroWalletService extends WalletService< final currentWalletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); final currentWallet = - WowneroWallet(walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + WowneroWallet(walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password); await currentWallet.renameWalletFiles(newName); @@ -235,7 +235,7 @@ class WowneroWalletService extends WalletService< viewKey: credentials.viewKey, spendKey: credentials.spendKey); final wallet = WowneroWallet( - walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); await wallet.init(); return wallet; @@ -268,7 +268,7 @@ class WowneroWalletService extends WalletService< seed: credentials.mnemonic, restoreHeight: credentials.height!); final wallet = WowneroWallet( - walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); await wallet.init(); return wallet; @@ -315,7 +315,7 @@ class WowneroWalletService extends WalletService< restoreHeight: height, spendKey: spendKey); - final wallet = WowneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + final wallet = WowneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password); await wallet.init(); return wallet; diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 737743925..85d856b35 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -113,6 +113,15 @@ packages: url: "https://pub.dev" source: hosted version: "8.4.3" + cake_backup: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" characters: dependency: transitive description: @@ -169,6 +178,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" cw_core: dependency: "direct main" description: @@ -680,6 +705,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index edfc77acb..989cd2b35 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -29,8 +29,8 @@ class CWBitcoin extends Bitcoin { @override WalletCredentials createBitcoinNewWalletCredentials( - {required String name, WalletInfo? walletInfo}) => - BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo); + {required String name, WalletInfo? walletInfo, String? password}) => + BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); @override WalletCredentials createBitcoinHardwareWalletCredentials( @@ -203,13 +203,13 @@ class CWBitcoin extends Bitcoin { } WalletService createBitcoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool alwaysScan) { - return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan); + Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect) { + return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); } WalletService createLitecoinWalletService( - Box walletInfoSource, Box unspentCoinSource) { - return LitecoinWalletService(walletInfoSource, unspentCoinSource); + Box walletInfoSource, Box unspentCoinSource, bool isDirect) { + return LitecoinWalletService(walletInfoSource, unspentCoinSource, isDirect); } @override diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index 6e169209f..fcb34a286 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -6,16 +6,17 @@ class CWBitcoinCash extends BitcoinCash { @override WalletService createBitcoinCashWalletService( - Box walletInfoSource, Box unspentCoinSource) { - return BitcoinCashWalletService(walletInfoSource, unspentCoinSource); + Box walletInfoSource, Box unspentCoinSource, bool isDirect) { + return BitcoinCashWalletService(walletInfoSource, unspentCoinSource, isDirect); } @override WalletCredentials createBitcoinCashNewWalletCredentials({ required String name, WalletInfo? walletInfo, + String? password, }) => - BitcoinCashNewWalletCredentials(name: name, walletInfo: walletInfo); + BitcoinCashNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); @override WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 577238baf..42e24d3c7 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cw_core/root_dir.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cw_core/wallet_type.dart'; @@ -20,7 +21,6 @@ import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/wallet_types.g.dart'; - import 'package:cake_backup/backup.dart' as cake_backup; class BackupService { diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 2f3acb6c9..823aa7e84 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -15,7 +15,6 @@ import 'package:cw_core/wallet_type.dart'; class WalletCreationService { WalletCreationService( {required WalletType initialType, - required this.secureStorage, required this.keyService, required this.sharedPreferences, required this.settingsStore, @@ -25,7 +24,6 @@ class WalletCreationService { } WalletType type; - final SecureStorage secureStorage; final SharedPreferences sharedPreferences; final SettingsStore settingsStore; final KeyService keyService; @@ -56,12 +54,16 @@ class WalletCreationService { Future create(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); - final password = generateWalletPassword(); - credentials.password = password; + + if (credentials.password == null) { + credentials.password = generateWalletPassword(); + await keyService.saveWalletPassword( + password: credentials.password!, walletName: credentials.name); + } + if (_hasSeedPhraseLengthOption) { credentials.seedPhraseLength = settingsStore.seedPhraseLength.value; } - await keyService.saveWalletPassword(password: password, walletName: credentials.name); final wallet = await _service!.create(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { @@ -94,9 +96,13 @@ class WalletCreationService { Future restoreFromKeys(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); - final password = generateWalletPassword(); - credentials.password = password; - await keyService.saveWalletPassword(password: password, walletName: credentials.name); + + if (credentials.password == null) { + credentials.password = generateWalletPassword(); + await keyService.saveWalletPassword( + password: credentials.password!, walletName: credentials.name); + } + final wallet = await _service!.restoreFromKeys(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { @@ -109,9 +115,13 @@ class WalletCreationService { Future restoreFromSeed(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); - final password = generateWalletPassword(); - credentials.password = password; - await keyService.saveWalletPassword(password: password, walletName: credentials.name); + + if (credentials.password == null) { + credentials.password = generateWalletPassword(); + await keyService.saveWalletPassword( + password: credentials.password!, walletName: credentials.name); + } + final wallet = await _service!.restoreFromSeed(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index ca29576e4..2b570f14c 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -20,17 +20,18 @@ class WalletLoadingService { final KeyService keyService; final WalletService Function(WalletType type) walletServiceFactory; - Future renameWallet(WalletType type, String name, String newName) async { + Future renameWallet(WalletType type, String name, String newName, + {String? password}) async { final walletService = walletServiceFactory.call(type); - final password = await keyService.getWalletPassword(walletName: name); + 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: password); + 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); - await walletService.rename(name, password, newName); + await walletService.rename(name, walletPassword, newName); // set shared preferences flag based on previous wallet name if (type == WalletType.monero) { @@ -41,11 +42,11 @@ class WalletLoadingService { } } - Future load(WalletType type, String name) async { + Future load(WalletType type, String name, {String? password}) async { try { final walletService = walletServiceFactory.call(type); - final password = await keyService.getWalletPassword(walletName: name); - final wallet = await walletService.openWallet(name, password); + final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name)); + final wallet = await walletService.openWallet(name, walletPassword); if (type == WalletType.monero) { await updateMoneroWalletPassword(wallet); @@ -67,8 +68,8 @@ class WalletLoadingService { for (var walletInfo in walletInfoSource.values) { try { final walletService = walletServiceFactory.call(walletInfo.type); - final password = await keyService.getWalletPassword(walletName: walletInfo.name); - final wallet = await walletService.openWallet(walletInfo.name, password); + final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name)); + final wallet = await walletService.openWallet(walletInfo.name, walletPassword); if (walletInfo.type == WalletType.monero) { await updateMoneroWalletPassword(wallet); diff --git a/lib/di.dart b/lib/di.dart index a64270f6d..7c22e809c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -30,6 +30,7 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; @@ -113,6 +114,8 @@ import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_page.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; @@ -217,6 +220,8 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_unlock_loadable_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_unlock_verifiable_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/receive_page_option.dart'; @@ -337,7 +342,6 @@ Future setup({ WalletCreationService( initialType: type, keyService: getIt.get(), - secureStorage: getIt.get(), sharedPreferences: getIt.get(), settingsStore: getIt.get(), walletInfoSource: _walletInfoSource)); @@ -357,6 +361,65 @@ Future setup({ getIt.get(param1: type), type: type)); + getIt.registerFactoryParam((args, closable) { + return WalletUnlockPage( + getIt.get(param1: args), + args.callback, + args.authPasswordHandler, + closable: closable); + }, instanceName: 'wallet_unlock_loadable'); + + getIt.registerFactory( + () => getIt.get( + param1: WalletUnlockArguments( + callback: (bool successful, _) { + if (successful) { + final authStore = getIt.get(); + authStore.allowed(); + }}), + param2: false, + instanceName: 'wallet_unlock_loadable'), + instanceName: 'wallet_password_login'); + + getIt.registerFactoryParam((args, closable) { + return WalletUnlockPage( + getIt.get(param1: args), + args.callback, + args.authPasswordHandler, + closable: closable); + }, instanceName: 'wallet_unlock_verifiable'); + + getIt.registerFactoryParam((args, _) { + final currentWalletName = getIt + .get() + .getString(PreferencesKey.currentWalletName) ?? ''; + final currentWalletTypeRaw = + getIt.get() + .getInt(PreferencesKey.currentWalletType) ?? 0; + final currentWalletType = deserializeFromInt(currentWalletTypeRaw); + + return WalletUnlockLoadableViewModel( + getIt.get(), + getIt.get(), + walletName: args.walletName ?? currentWalletName, + walletType: args.walletType ?? currentWalletType); + }); + + getIt.registerFactoryParam((args, _) { + final currentWalletName = getIt + .get() + .getString(PreferencesKey.currentWalletName) ?? ''; + final currentWalletTypeRaw = + getIt.get() + .getInt(PreferencesKey.currentWalletType) ?? 0; + final currentWalletType = deserializeFromInt(currentWalletTypeRaw); + + return WalletUnlockVerifiableViewModel( + getIt.get(), + walletName: args.walletName ?? currentWalletName, + walletType: args.walletType ?? currentWalletType); + }); + getIt.registerFactoryParam((WalletType type, _) { return WalletRestorationFromQRVM(getIt.get(), getIt.get(param1: type), _walletInfoSource, type); @@ -907,23 +970,28 @@ Future setup({ _walletInfoSource, _unspentCoinsInfoSource, getIt.get().silentPaymentsAlwaysScan, + SettingsStoreBase.walletPasswordDirectInput, ); case WalletType.litecoin: - return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource); + return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource, + SettingsStoreBase.walletPasswordDirectInput); case WalletType.ethereum: - return ethereum!.createEthereumWalletService(_walletInfoSource); + return ethereum!.createEthereumWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.bitcoinCash: - return bitcoinCash! - .createBitcoinCashWalletService(_walletInfoSource, _unspentCoinsInfoSource); + return bitcoinCash!.createBitcoinCashWalletService(_walletInfoSource, + _unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.nano: case WalletType.banano: - return nano!.createNanoWalletService(_walletInfoSource); + return nano!.createNanoWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.polygon: - return polygon!.createPolygonWalletService(_walletInfoSource); + return polygon!.createPolygonWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.solana: - return solana!.createSolanaWalletService(_walletInfoSource); + return solana!.createSolanaWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.tron: - return tron!.createTronWalletService(_walletInfoSource); + return tron!.createTronWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.wownero: return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.none: diff --git a/lib/entities/load_current_wallet.dart b/lib/entities/load_current_wallet.dart index 595bc2233..e67b59997 100644 --- a/lib/entities/load_current_wallet.dart +++ b/lib/entities/load_current_wallet.dart @@ -6,7 +6,7 @@ import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; -Future loadCurrentWallet() async { +Future loadCurrentWallet({String? password}) async { final appStore = getIt.get(); final name = getIt .get() @@ -21,7 +21,10 @@ Future loadCurrentWallet() async { final type = deserializeFromInt(typeRaw); final walletLoadingService = getIt.get(); - final wallet = await walletLoadingService.load(type, name); + final wallet = await walletLoadingService.load( + type, + name, + password: password); await appStore.changeCurrentWallet(wallet); getIt.get().registerSyncTask(); diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 7b593d58d..4e210b227 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -4,15 +4,16 @@ class CWEthereum extends Ethereum { @override List getEthereumWordList(String language) => EVMChainMnemonics.englishWordlist; - WalletService createEthereumWalletService(Box walletInfoSource) => - EthereumWalletService(walletInfoSource, client: EthereumClient()); + WalletService createEthereumWalletService(Box walletInfoSource, bool isDirect) => + EthereumWalletService(walletInfoSource, isDirect, client: EthereumClient()); @override WalletCredentials createEthereumNewWalletCredentials({ required String name, WalletInfo? walletInfo, + String? password, }) => - EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo); + EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); @override WalletCredentials createEthereumRestoreWalletFromSeedCredentials({ diff --git a/lib/main.dart b/lib/main.dart index 1c0078e16..aeb76b3a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,6 +48,7 @@ final rootKey = GlobalKey(); final RouteObserver> routeObserver = RouteObserver>(); Future main() async { + bool isAppRunning = false; await runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); @@ -170,7 +171,6 @@ Future initializeAppConfigs() async { } final secureStorage = secureStorageShared; - final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey); final tradesBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Trade.boxKey); @@ -247,8 +247,8 @@ Future initialSetup( ordersSource: ordersSource, anonpayInvoiceInfoSource: anonpayInvoiceInfo, unspentCoinsInfoSource: unspentCoinsInfoSource, - secureStorage: secureStorage, navigatorKey: navigatorKey, + secureStorage: secureStorage, ); await bootstrap(navigatorKey); monero?.onStartup(); diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index ad02d2ccb..8cf640d8b 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -75,8 +75,8 @@ class CWNano extends Nano { } @override - WalletService createNanoWalletService(Box walletInfoSource) { - return NanoWalletService(walletInfoSource); + WalletService createNanoWalletService(Box walletInfoSource, bool isDirect) { + return NanoWalletService(walletInfoSource, isDirect); } @override diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 2dcb1b4a6..5bb87ff5b 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -4,15 +4,16 @@ class CWPolygon extends Polygon { @override List getPolygonWordList(String language) => EVMChainMnemonics.englishWordlist; - WalletService createPolygonWalletService(Box walletInfoSource) => - PolygonWalletService(walletInfoSource, client: PolygonClient()); + WalletService createPolygonWalletService(Box walletInfoSource, bool isDirect) => + PolygonWalletService(walletInfoSource, isDirect, client: PolygonClient()); @override WalletCredentials createPolygonNewWalletCredentials({ required String name, WalletInfo? walletInfo, + String? password }) => - EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo); + EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); @override WalletCredentials createPolygonRestoreWalletFromSeedCredentials({ diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index e4fd9b32f..95cbd51df 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:flutter/widgets.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/load_current_wallet.dart'; @@ -23,7 +24,7 @@ void startAuthenticationStateChange( _onAuthenticationStateChange ??= autorun((_) async { final state = authenticationStore.state; - if (state == AuthenticationState.installed) { + if (state == AuthenticationState.installed && !SettingsStoreBase.walletPasswordDirectInput) { try { await loadCurrentWallet(); } catch (error, stack) { diff --git a/lib/router.dart b/lib/router.dart index c09664cef..498077511 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -90,7 +90,11 @@ import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_page.dart'; import 'package:cake_wallet/src/screens/welcome/create_welcome_page.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -125,6 +129,14 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute(builder: (_) => createWelcomePage()); case Routes.newWalletFromWelcome: + if (SettingsStoreBase.walletPasswordDirectInput) { + if (availableWalletTypes.length == 1) { + return createRoute(RouteSettings(name: Routes.newWallet, arguments: availableWalletTypes.first)); + } else { + return createRoute(RouteSettings(name: Routes.newWalletType)); + } + } + return CupertinoPageRoute( builder: (_) => getIt.get(param1: (PinCodeState context, dynamic _) { @@ -176,6 +188,10 @@ Route createRoute(RouteSettings settings) { param2: [false, false])); case Routes.restoreOptions: + if (SettingsStoreBase.walletPasswordDirectInput) { + return createRoute(RouteSettings(name: Routes.restoreWalletType)); + } + final isNewInstall = settings.arguments as bool; return CupertinoPageRoute( fullscreenDialog: true, @@ -328,8 +344,16 @@ Route createRoute(RouteSettings settings) { case Routes.auth: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get( - param1: settings.arguments as OnAuthenticationFinished, param2: true)); + builder: (_) + => SettingsStoreBase.walletPasswordDirectInput + ? getIt.get( + param1: WalletUnlockArguments( + callback: settings.arguments as OnAuthenticationFinished), + instanceName: 'wallet_unlock_verifiable', + param2: true) + : getIt.get( + param1: settings.arguments as OnAuthenticationFinished, + param2: true)); case Routes.totpAuthCodePage: final args = settings.arguments as TotpAuthArgumentsModel; @@ -340,24 +364,32 @@ Route createRoute(RouteSettings settings) { ), ); - case Routes.login: - return CupertinoPageRoute( - builder: (context) => WillPopScope( - child: getIt.get(instanceName: 'login'), - onWillPop: () async => - // FIX-ME: Additional check does it works correctly - (await SystemChannels.platform.invokeMethod('SystemNavigator.pop') ?? - false), - ), - fullscreenDialog: true); + case Routes.walletUnlockLoadable: + return MaterialPageRoute( + fullscreenDialog: true, + builder: (_) + => getIt.get( + param1: settings.arguments as WalletUnlockArguments, + instanceName: 'wallet_unlock_loadable', + param2: true)); case Routes.unlock: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => WillPopScope( - child: getIt.get( - param1: settings.arguments as OnAuthenticationFinished, param2: false), - onWillPop: () async => false)); + builder: (_) + => SettingsStoreBase.walletPasswordDirectInput + ? WillPopScope( + child: getIt.get( + param1: WalletUnlockArguments( + callback: settings.arguments as OnAuthenticationFinished), + param2: false, + instanceName: 'wallet_unlock_verifiable'), + onWillPop: () async => false) + : WillPopScope( + child: getIt.get( + param1: settings.arguments as OnAuthenticationFinished, + param2: false), + onWillPop: () async => false)); case Routes.silentPaymentsSettings: return CupertinoPageRoute( @@ -397,6 +429,17 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get( param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?)); + case Routes.login: + return CupertinoPageRoute( + builder: (context) => WillPopScope( + child: SettingsStoreBase.walletPasswordDirectInput + ? getIt.get(instanceName: 'wallet_password_login') + : getIt.get(instanceName: 'login'), + onWillPop: () async => + // FIX-ME: Additional check does it works correctly + (await SystemChannels.platform.invokeMethod('SystemNavigator.pop') ?? false)), + fullscreenDialog: true); + case Routes.newPowNode: final args = settings.arguments as Map?; return CupertinoPageRoute( @@ -486,7 +529,9 @@ Route createRoute(RouteSettings settings) { fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.support: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + fullscreenDialog: true, + builder: (_) => getIt.get()); case Routes.supportLiveChat: return CupertinoPageRoute(builder: (_) => getIt.get()); diff --git a/lib/routes.dart b/lib/routes.dart index 78a93bee7..caa7eb39e 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -82,6 +82,8 @@ class Routes { static const otherSettingsPage = '/other_settings_page'; static const advancedPrivacySettings = '/advanced_privacy_settings'; static const sweepingWalletPage = '/sweeping_wallet_page'; + static const walletPasswordUnlock = '/wallet_password_unlock'; + static const walletUnlockLoadable = '/wallet_unlock_loadable'; static const anonPayInvoicePage = '/anon_pay_invoice_page'; static const anonPayReceivePage = '/anon_pay_receive_page'; static const anonPayDetailsPage = '/anon_pay_details_page'; diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index af66cf3e5..e70739db9 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -4,15 +4,16 @@ class CWSolana extends Solana { @override List getSolanaWordList(String language) => SolanaMnemonics.englishWordlist; - WalletService createSolanaWalletService(Box walletInfoSource) => - SolanaWalletService(walletInfoSource); + WalletService createSolanaWalletService(Box walletInfoSource, bool isDirect) => + SolanaWalletService(walletInfoSource, isDirect); @override WalletCredentials createSolanaNewWalletCredentials({ required String name, WalletInfo? walletInfo, + String? password, }) => - SolanaNewWalletCredentials(name: name, walletInfo: walletInfo); + SolanaNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); @override WalletCredentials createSolanaRestoreWalletFromSeedCredentials({ diff --git a/lib/src/screens/auth/auth_page.dart b/lib/src/screens/auth/auth_page.dart index dcd1c8016..d14a12527 100644 --- a/lib/src/screens/auth/auth_page.dart +++ b/lib/src/screens/auth/auth_page.dart @@ -12,6 +12,12 @@ import 'package:cake_wallet/core/execution_state.dart'; typedef OnAuthenticationFinished = void Function(bool, AuthPageState); +abstract class AuthPageState extends State { + void changeProcessText(String text); + void hideProgressText(); + Future close({String? route, dynamic arguments}); +} + class AuthPage extends StatefulWidget { AuthPage(this.authViewModel, {required this.onAuthenticationFinished, @@ -22,10 +28,10 @@ class AuthPage extends StatefulWidget { final bool closable; @override - AuthPageState createState() => AuthPageState(); + AuthPageState createState() => AuthPagePinCodeStateImpl(); } -class AuthPageState extends State { +class AuthPagePinCodeStateImpl extends AuthPageState { final _key = GlobalKey(); final _pinCodeKey = GlobalKey(); final _backArrowImageDarkTheme = @@ -55,8 +61,6 @@ class AuthPageState extends State { } if (state is FailureState) { - print('X'); - print(state.error); WidgetsBinding.instance.addPostFrameCallback((_) async { _pinCodeKey.currentState?.clear(); dismissFlushBar(_authBar); @@ -95,17 +99,20 @@ class AuthPageState extends State { super.dispose(); } + @override void changeProcessText(String text) { dismissFlushBar(_authBar); _progressBar = createBar(text, duration: null) ..show(_key.currentContext!); } + @override void hideProgressText() { dismissFlushBar(_progressBar); _progressBar = null; } + @override Future close({String? route, dynamic arguments}) async { if (_key.currentContext == null) { throw Exception('Key context is null. Should be not happened'); diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_action_button.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_action_button.dart index 7e9b2b23d..f49047e0b 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_action_button.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_action_button.dart @@ -2,6 +2,9 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; +import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:flutter/material.dart'; class DesktopActionButton extends StatelessWidget { @@ -24,45 +27,48 @@ class DesktopActionButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), - child: GestureDetector( - onTap: onTap, - child: Container( - padding: EdgeInsets.symmetric(vertical: 25), - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15.0), - color: Theme.of(context).extension()!.syncedBackgroundColor, - ), - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - image, - height: 30, - width: 30, - color: isEnabled - ? Theme.of(context).extension()!.textColor - : Theme.of(context).extension()!.labelTextColor, - ), - const SizedBox(width: 10), - AutoSizeText( - title, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.bold, + return MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), + child: GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(vertical: 25), + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15.0), + color: Theme.of(context).extension()!.syncedBackgroundColor, + ), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + image, + height: 30, + width: 30, color: isEnabled ? Theme.of(context).extension()!.textColor - : null, - height: 1, + : Theme.of(context).extension()!.labelTextColor, ), - maxLines: 1, - textAlign: TextAlign.center, - ) - ], + const SizedBox(width: 10), + AutoSizeText( + title, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.bold, + color: isEnabled + ? Theme.of(context).extension()!.textColor + : null, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ) + ], + ), ), ), ), diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 46e63af01..94489a945 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -4,9 +4,12 @@ import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/desktop_dropdown_item.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; @@ -176,12 +179,25 @@ class _DesktopWalletSelectionDropDownState extends State _loadWallet(WalletListItem wallet) async { - widget._authService.authenticateAction( - context, - onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) { - return; - } + if (SettingsStoreBase.walletPasswordDirectInput) { + Navigator.of(context).pushNamed( + Routes.walletUnlockLoadable, + arguments: WalletUnlockArguments( + callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { + if (isAuthenticatedSuccessfully) { + auth.close(); + setState(() {}); + } + }, walletName: wallet.name, + walletType: wallet.type)); + return; + } + + widget._authService.authenticateAction(context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (!isAuthenticatedSuccessfully) { + return; + } try { if (context.mounted) { diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index d9427af0a..b66aab4cf 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -68,14 +68,19 @@ class _WalletNameFormState extends State { _WalletNameFormState(this._walletNewVM) : _formKey = GlobalKey(), _languageSelectorKey = GlobalKey(), - _controller = TextEditingController(); + _nameController = TextEditingController(), + _passwordController = _walletNewVM.hasWalletPassword ? TextEditingController() : null, + _repeatedPasswordController = + _walletNewVM.hasWalletPassword ? TextEditingController() : null; static const aspectRatioImage = 1.22; final GlobalKey _formKey; final GlobalKey _languageSelectorKey; final WalletNewVM _walletNewVM; - final TextEditingController _controller; + final TextEditingController _nameController; + final TextEditingController? _passwordController; + final TextEditingController? _repeatedPasswordController; ReactionDisposer? _stateReaction; @override @@ -130,12 +135,11 @@ class _WalletNameFormState extends State { padding: EdgeInsets.only(top: 24), child: Form( key: _formKey, - child: Stack( - alignment: Alignment.centerRight, + child: Column( children: [ TextFormField( onChanged: (value) => _walletNewVM.name = value, - controller: _controller, + controller: _nameController, textAlign: TextAlign.center, style: TextStyle( fontSize: 20.0, @@ -169,10 +173,10 @@ class _WalletNameFormState extends State { FocusManager.instance.primaryFocus?.unfocus(); setState(() { - _controller.text = rName; + _nameController.text = rName; _walletNewVM.name = rName; - _controller.selection = TextSelection.fromPosition( - TextPosition(offset: _controller.text.length)); + _nameController.selection = TextSelection.fromPosition( + TextPosition(offset: _nameController.text.length)); }); }, icon: Container( @@ -195,6 +199,80 @@ class _WalletNameFormState extends State { ), validator: WalletNameValidator(), ), + if (_walletNewVM.hasWalletPassword) ...[ + TextFormField( + onChanged: (value) => _walletNewVM.walletPassword = value, + controller: _passwordController, + textAlign: TextAlign.center, + obscureText: true, + style: TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + ), + decoration: InputDecoration( + hintStyle: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + color: + Theme.of(context).extension()!.hintTextColor, + ), + hintText: S.of(context).password, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .extension()! + .underlineColor, + width: 1.0, + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .extension()! + .underlineColor, + width: 1.0, + ), + ), + ), + ), + TextFormField( + onChanged: (value) => _walletNewVM.repeatedWalletPassword = value, + controller: _repeatedPasswordController, + textAlign: TextAlign.center, + obscureText: true, + style: TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + ), + decoration: InputDecoration( + hintStyle: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + color: + Theme.of(context).extension()!.hintTextColor, + ), + hintText: S.of(context).repeat_wallet_password, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .extension()! + .underlineColor, + width: 1.0, + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .extension()! + .underlineColor, + width: 1.0, + ), + ), + ), + ), + ], ], ), ), diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index f8336a2e8..56e49b087 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -16,6 +16,9 @@ class WalletRestoreFromKeysFrom extends StatefulWidget { required this.onPrivateKeyChange, required this.displayPrivateKeyField, required this.onHeightOrDateEntered, + required this.displayWalletPassword, + required this.onRepeatedPasswordChange, + this.onPasswordChange, Key? key, }) : super(key: key); @@ -23,13 +26,17 @@ class WalletRestoreFromKeysFrom extends StatefulWidget { final WalletRestoreViewModel walletRestoreViewModel; final void Function(String)? onPrivateKeyChange; final bool displayPrivateKeyField; + final bool displayWalletPassword; + final void Function(String)? onPasswordChange; + final void Function(String)? onRepeatedPasswordChange; @override - WalletRestoreFromKeysFromState createState() => WalletRestoreFromKeysFromState(); + WalletRestoreFromKeysFromState createState() => + WalletRestoreFromKeysFromState(displayWalletPassword: displayWalletPassword); } class WalletRestoreFromKeysFromState extends State { - WalletRestoreFromKeysFromState() + WalletRestoreFromKeysFromState({required bool displayWalletPassword}) : formKey = GlobalKey(), blockchainHeightKey = GlobalKey(), nameController = TextEditingController(), @@ -37,7 +44,9 @@ class WalletRestoreFromKeysFromState extends State { viewKeyController = TextEditingController(), spendKeyController = TextEditingController(), privateKeyController = TextEditingController(), - nameTextEditingController = TextEditingController(); + nameTextEditingController = TextEditingController(), + passwordTextEditingController = displayWalletPassword ? TextEditingController() : null, + repeatedPasswordTextEditingController = displayWalletPassword ? TextEditingController() : null; final GlobalKey formKey; final GlobalKey blockchainHeightKey; @@ -47,9 +56,22 @@ class WalletRestoreFromKeysFromState extends State { final TextEditingController spendKeyController; final TextEditingController nameTextEditingController; final TextEditingController privateKeyController; + final TextEditingController? passwordTextEditingController; + final TextEditingController? repeatedPasswordTextEditingController; + void Function()? passwordListener; + void Function()? repeatedPasswordListener; @override void initState() { + if (passwordTextEditingController != null) { + passwordListener = () => widget.onPasswordChange?.call(passwordTextEditingController!.text); + passwordTextEditingController?.addListener(passwordListener!); + } + + if (repeatedPasswordTextEditingController != null) { + repeatedPasswordListener = () => widget.onRepeatedPasswordChange?.call(repeatedPasswordTextEditingController!.text); + repeatedPasswordTextEditingController?.addListener(repeatedPasswordListener!); + } super.initState(); privateKeyController.addListener(() { @@ -67,6 +89,14 @@ class WalletRestoreFromKeysFromState extends State { viewKeyController.dispose(); privateKeyController.dispose(); spendKeyController.dispose(); + passwordTextEditingController?.dispose(); + if (passwordListener != null) { + passwordTextEditingController?.removeListener(passwordListener!); + } + + if (repeatedPasswordListener != null) { + repeatedPasswordTextEditingController?.removeListener(repeatedPasswordListener!); + } super.dispose(); } @@ -114,6 +144,19 @@ class WalletRestoreFromKeysFromState extends State { ), ], ), + if (widget.displayWalletPassword) + ...[Container( + padding: EdgeInsets.only(top: 20.0), + child: BaseTextFormField( + controller: passwordTextEditingController, + hintText: S.of(context).password, + obscureText: true)), + Container( + padding: EdgeInsets.only(top: 20.0), + child: BaseTextFormField( + controller: repeatedPasswordTextEditingController, + hintText: S.of(context).repeat_wallet_password, + obscureText: true))], Container(height: 20), _restoreFromKeysFormFields(), ], 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 1f22af0cb..ec40eb1c1 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -22,34 +22,43 @@ class WalletRestoreFromSeedForm extends StatefulWidget { required this.displayBlockHeightSelector, required this.displayPassphrase, required this.type, + required this.displayWalletPassword, required this.seedTypeViewModel, this.blockHeightFocusNode, this.onHeightOrDateEntered, this.onSeedChange, - this.onLanguageChange}) + this.onLanguageChange, + this.onPasswordChange, + this.onRepeatedPasswordChange}) : super(key: key); final WalletType type; final bool displayLanguageSelector; final bool displayBlockHeightSelector; + final bool displayWalletPassword; final bool displayPassphrase; final SeedTypeViewModel seedTypeViewModel; final FocusNode? blockHeightFocusNode; final Function(bool)? onHeightOrDateEntered; final void Function(String)? onSeedChange; final void Function(String)? onLanguageChange; + final void Function(String)? onPasswordChange; + final void Function(String)? onRepeatedPasswordChange; @override - WalletRestoreFromSeedFormState createState() => WalletRestoreFromSeedFormState('English'); + WalletRestoreFromSeedFormState createState() => + WalletRestoreFromSeedFormState('English', displayWalletPassword: displayWalletPassword); } class WalletRestoreFromSeedFormState extends State { - WalletRestoreFromSeedFormState(this.language) + WalletRestoreFromSeedFormState(this.language, {required bool displayWalletPassword}) : seedWidgetStateKey = GlobalKey(), blockchainHeightKey = GlobalKey(), formKey = GlobalKey(), languageController = TextEditingController(), nameTextEditingController = TextEditingController(), + passwordTextEditingController = displayWalletPassword ? TextEditingController() : null, + repeatedPasswordTextEditingController = displayWalletPassword ? TextEditingController() : null, passphraseController = TextEditingController(), seedTypeController = TextEditingController(); @@ -57,16 +66,30 @@ class WalletRestoreFromSeedFormState extends State { final GlobalKey blockchainHeightKey; final TextEditingController languageController; final TextEditingController nameTextEditingController; + final TextEditingController? passwordTextEditingController; + final TextEditingController? repeatedPasswordTextEditingController; final TextEditingController seedTypeController; final TextEditingController passphraseController; final GlobalKey formKey; late ReactionDisposer moneroSeedTypeReaction; String language; + void Function()? passwordListener; + void Function()? repeatedPasswordListener; @override void initState() { _setSeedType(widget.seedTypeViewModel.moneroSeedType); _setLanguageLabel(language); + + if (passwordTextEditingController != null) { + passwordListener = () => widget.onPasswordChange?.call(passwordTextEditingController!.text); + passwordTextEditingController?.addListener(passwordListener!); + } + + if (repeatedPasswordTextEditingController != null) { + repeatedPasswordListener = () => widget.onRepeatedPasswordChange?.call(repeatedPasswordTextEditingController!.text); + repeatedPasswordTextEditingController?.addListener(repeatedPasswordListener!); + } moneroSeedTypeReaction = reaction((_) => widget.seedTypeViewModel.moneroSeedType, (SeedType item) { _setSeedType(item); @@ -78,8 +101,16 @@ class WalletRestoreFromSeedFormState extends State { @override void dispose() { - super.dispose(); moneroSeedTypeReaction(); + + if (passwordListener != null) { + passwordTextEditingController?.removeListener(passwordListener!); + } + + if (repeatedPasswordListener != null) { + repeatedPasswordTextEditingController?.removeListener(repeatedPasswordListener!); + } + super.dispose(); } void onSeedChange(String seed) { @@ -177,6 +208,16 @@ class WalletRestoreFromSeedFormState extends State { ), ), ), + if (widget.displayWalletPassword) + ...[BaseTextFormField( + controller: passwordTextEditingController, + hintText: S.of(context).password, + obscureText: true), + BaseTextFormField( + controller: repeatedPasswordTextEditingController, + hintText: S.of(context).repeat_wallet_password, + obscureText: true)], + if (widget.displayLanguageSelector) if (!seedTypeController.value.text.contains("14") && widget.displayLanguageSelector) GestureDetector( onTap: () async { diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 746b73dca..cd6383c0d 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -53,7 +53,10 @@ class WalletRestorePage extends BasePage { onLanguageChange: (String language) { final isPolyseed = language.startsWith("POLYSEED_"); _validateOnChange(isPolyseed: isPolyseed); - })); + }, + displayWalletPassword: walletRestoreViewModel.hasWalletPassword, + onPasswordChange: (String password) => walletRestoreViewModel.walletPassword = password, + onRepeatedPasswordChange: (String repeatedPassword) => walletRestoreViewModel.repeatedWalletPassword = repeatedPassword)); break; case WalletRestoreMode.keys: _pages.add(WalletRestoreFromKeysFrom( @@ -66,6 +69,9 @@ class WalletRestorePage extends BasePage { } }, displayPrivateKeyField: walletRestoreViewModel.hasRestoreFromPrivateKey, + displayWalletPassword: walletRestoreViewModel.hasWalletPassword, + onPasswordChange: (String password) => walletRestoreViewModel.walletPassword = password, + onRepeatedPasswordChange: (String repeatedPassword) => walletRestoreViewModel.repeatedWalletPassword = repeatedPassword, onHeightOrDateEntered: (value) => walletRestoreViewModel.isButtonEnabled = value)); break; default: @@ -127,6 +133,8 @@ class WalletRestorePage extends BasePage { reaction((_) => walletRestoreViewModel.mode, (WalletRestoreMode mode) { walletRestoreViewModel.isButtonEnabled = false; + walletRestoreViewModel.walletPassword = null; + walletRestoreViewModel.repeatedWalletPassword = null; walletRestoreFromSeedFormKey .currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = ''; diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 7ad8af4c5..8ce0ddde9 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/utils/device_info.dart'; diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index 470f49190..04ae53d77 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arro import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -41,15 +42,16 @@ class SecurityBackupPage extends BasePage { _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), ), - SettingsCellWithArrow( - title: S.current.create_backup, - handler: (_) => _authService.authenticateAction( - context, - route: Routes.backup, - conditionToDetermineIfToUse2FA: _securitySettingsViewModel - .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + if (!SettingsStoreBase.walletPasswordDirectInput) + SettingsCellWithArrow( + title: S.current.create_backup, + handler: (_) => _authService.authenticateAction( + context, + route: Routes.backup, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ), ), - ), SettingsCellWithArrow( title: S.current.settings_change_pin, handler: (_) => _authService.authenticateAction( @@ -119,6 +121,5 @@ class SecurityBackupPage extends BasePage { ], ), ); - } } diff --git a/lib/src/screens/wallet/wallet_edit_page.dart b/lib/src/screens/wallet/wallet_edit_page.dart index 7c90ba2c5..2d1bb9e47 100644 --- a/lib/src/screens/wallet/wallet_edit_page.dart +++ b/lib/src/screens/wallet/wallet_edit_page.dart @@ -4,6 +4,12 @@ import 'package:cake_wallet/core/wallet_name_validator.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; @@ -94,9 +100,38 @@ class WalletEditPage extends BasePage { ); } else { try { - await walletEditViewModel.changeName(editingWallet); - Navigator.of(context).pop(); - walletEditViewModel.resetState(); + bool confirmed = false; + + if (SettingsStoreBase + .walletPasswordDirectInput) { + await Navigator.of(context).pushNamed( + Routes.walletUnlockLoadable, + arguments: WalletUnlockArguments( + authPasswordHandler: + (String password) async { + await walletEditViewModel + .changeName(editingWallet, + password: password); + }, + callback: (bool + isAuthenticatedSuccessfully, + AuthPageState auth) async { + if (isAuthenticatedSuccessfully) { + auth.close(); + confirmed = true; + } + }, + walletName: editingWallet.name, + walletType: editingWallet.type)); + } else { + await walletEditViewModel.changeName(editingWallet); + confirmed = true; + } + + if (confirmed) { + Navigator.of(context).pop(); + walletEditViewModel.resetState(); + } } catch (e) {} } } @@ -120,11 +155,11 @@ class WalletEditPage extends BasePage { Future _removeWallet(BuildContext context) async { authService.authenticateAction(context, onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) { - return; - } + if (!isAuthenticatedSuccessfully) { + return; + } - _onSuccessfulAuth(context); + _onSuccessfulAuth(context); }, conditionToDetermineIfToUse2FA: false, ); diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 2a4841608..0d6d9e912 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -1,7 +1,10 @@ import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart'; import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; @@ -336,6 +339,20 @@ class WalletListBodyState extends State { } Future _loadWallet(WalletListItem wallet) async { + if (SettingsStoreBase.walletPasswordDirectInput) { + Navigator.of(context).pushNamed( + Routes.walletUnlockLoadable, + arguments: WalletUnlockArguments( + callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { + if (isAuthenticatedSuccessfully) { + auth.close(); + setState(() {}); + } + }, walletName: wallet.name, + walletType: wallet.type)); + return; + } + await widget.authService.authenticateAction( context, onAuthSuccess: (isAuthenticatedSuccessfully) async { diff --git a/lib/src/screens/wallet_unlock/wallet_unlock_arguments.dart b/lib/src/screens/wallet_unlock/wallet_unlock_arguments.dart new file mode 100644 index 000000000..5b6d4dd16 --- /dev/null +++ b/lib/src/screens/wallet_unlock/wallet_unlock_arguments.dart @@ -0,0 +1,17 @@ +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cw_core/wallet_type.dart'; + +typedef AuthPasswordHandler = Future Function(String); + +class WalletUnlockArguments { + WalletUnlockArguments( + {required this.callback, + this.walletName, + this.walletType, + this.authPasswordHandler}); + + final OnAuthenticationFinished callback; + final AuthPasswordHandler? authPasswordHandler; + final String? walletName; + final WalletType? walletType; +} diff --git a/lib/src/screens/wallet_unlock/wallet_unlock_page.dart b/lib/src/screens/wallet_unlock/wallet_unlock_page.dart new file mode 100644 index 000000000..3e6966f9d --- /dev/null +++ b/lib/src/screens/wallet_unlock/wallet_unlock_page.dart @@ -0,0 +1,238 @@ +import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.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/extensions/new_wallet_theme.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/view_model/wallet_unlock_view_model.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; + +class WalletUnlockPage extends StatefulWidget { + WalletUnlockPage( + this.walletUnlockViewModel, this.onAuthenticationFinished, this.authPasswordHandler, + {required this.closable}); + + final WalletUnlockViewModel walletUnlockViewModel; + final OnAuthenticationFinished onAuthenticationFinished; + final AuthPasswordHandler? authPasswordHandler; + final bool closable; + + @override + State createState() => WalletUnlockPageState(); +} + +class WalletUnlockPageState extends AuthPageState { + WalletUnlockPageState() : _passwordController = TextEditingController(); + + final TextEditingController _passwordController; + final _key = GlobalKey(); + final _backArrowImageDarkTheme = Image.asset('assets/images/close_button.png'); + ReactionDisposer? _reaction; + Flushbar? _authBar; + Flushbar? _progressBar; + void Function()? _passwordControllerListener; + + @override + void initState() { + _reaction ??= reaction((_) => widget.walletUnlockViewModel.state, (ExecutionState state) { + if (state is ExecutedSuccessfullyState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onAuthenticationFinished(true, this); + }); + setState(() {}); + } + + if (state is IsExecutingState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // null duration to make it indefinite until its disposed + _authBar = createBar(S.of(context).authentication, duration: null)..show(context); + }); + } + + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + dismissFlushBar(_authBar); + showBar(context, S.of(context).failed_authentication(state.error)); + + widget.onAuthenticationFinished(false, this); + }); + } + }); + + _passwordControllerListener = + () => widget.walletUnlockViewModel.setPassword(_passwordController.text); + + if (_passwordControllerListener != null) { + _passwordController.addListener(_passwordControllerListener!); + } + + super.initState(); + } + + @override + void dispose() { + _reaction?.reaction.dispose(); + + if (_passwordControllerListener != null) { + _passwordController.removeListener(_passwordControllerListener!); + } + + super.dispose(); + } + + @override + void changeProcessText(String text) { + dismissFlushBar(_authBar); + _progressBar = createBar(text, duration: null)..show(_key.currentContext!); + } + + @override + void hideProgressText() { + dismissFlushBar(_progressBar); + _progressBar = null; + } + + void dismissFlushBar(Flushbar? bar) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await bar?.dismiss(); + }); + } + + @override + Future close({String? route, arguments}) async { + if (_key.currentContext == null) { + throw Exception('Key context is null. Should be not happened'); + } + + /// not the best scenario, but WidgetsBinding is not behaving correctly on Android + await Future.delayed(Duration(milliseconds: 50)); + await _authBar?.dismiss(); + await Future.delayed(Duration(milliseconds: 50)); + await _progressBar?.dismiss(); + await Future.delayed(Duration(milliseconds: 50)); + if (route != null) { + Navigator.of(_key.currentContext!).pushReplacementNamed(route, arguments: arguments); + } else { + Navigator.of(_key.currentContext!).pop(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _key, + appBar: CupertinoNavigationBar( + leading: widget.closable + ? Container( + padding: EdgeInsets.only(top: 10), + child: SizedBox( + height: 37, + width: 37, + child: InkWell( + onTap: () => Navigator.of(context).pop(), + child: _backArrowImageDarkTheme, + ), + )) + : Container(), + backgroundColor: Theme.of(context).colorScheme.background, + border: null), + resizeToAvoidBottomInset: false, + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.walletUnlockViewModel.walletName, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ), + ), + SizedBox(height: 24), + Form( + child: TextFormField( + onChanged: (value) => null, + controller: _passwordController, + textAlign: TextAlign.center, + obscureText: true, + style: TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + ), + decoration: InputDecoration( + hintStyle: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.hintTextColor, + ), + hintText: S.of(context).enter_wallet_password, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).extension()!.underlineColor, + width: 1.0, + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).extension()!.underlineColor, + width: 1.0, + ), + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 24), + child: Observer( + builder: (_) => LoadingPrimaryButton( + onPressed: () async { + if (widget.authPasswordHandler != null) { + try { + await widget + .authPasswordHandler!(widget.walletUnlockViewModel.password); + widget.walletUnlockViewModel.success(); + } catch (e) { + widget.walletUnlockViewModel.failure(e); + } + return; + } + + widget.walletUnlockViewModel.unlock(); + }, + text: S.of(context).unlock, + color: Colors.green, + textColor: Colors.white, + isLoading: widget.walletUnlockViewModel.state is IsExecutingState, + isDisabled: widget.walletUnlockViewModel.state is IsExecutingState), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 0467b18a2..f79da8069 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -1,5 +1,5 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; @@ -19,7 +19,10 @@ class AddressTextField extends StatelessWidget { {required this.controller, this.isActive = true, this.placeholder, - this.options = const [AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook], + this.options = const [ + AddressTextFieldOption.qrCode, + AddressTextFieldOption.addressBook + ], this.onURIScanned, this.focusNode, this.isBorderExist = true, diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 8fb26df53..df2c7767f 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -553,6 +553,7 @@ abstract class SettingsStoreBase with Store { static const defaultActionsMode = 11; static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes; static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; + static final walletPasswordDirectInput = Platform.isLinux; static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; static const defaultMoneroSeedType = SeedType.defaultSeedType; diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index 52b4f59f7..c6ac89342 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -4,15 +4,17 @@ class CWTron extends Tron { @override List getTronWordList(String language) => EVMChainMnemonics.englishWordlist; - WalletService createTronWalletService(Box walletInfoSource) => - TronWalletService(walletInfoSource, client: TronClient()); + @override + WalletService createTronWalletService(Box walletInfoSource, bool isDirect) => + TronWalletService(walletInfoSource, client: TronClient(), isDirect: isDirect); @override WalletCredentials createTronNewWalletCredentials({ required String name, WalletInfo? walletInfo, + String? password, }) => - TronNewWalletCredentials(name: name, walletInfo: walletInfo); + TronNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); @override WalletCredentials createTronRestoreWalletFromSeedCredentials({ diff --git a/lib/utils/package_info.dart b/lib/utils/package_info.dart index 8b911f887..5f3f0e743 100644 --- a/lib/utils/package_info.dart +++ b/lib/utils/package_info.dart @@ -1,5 +1,5 @@ import 'dart:io'; -import 'package:package_info/package_info.dart' as __package_info__; +import 'package:package_info_plus/package_info_plus.dart' as __package_info__; abstract class _EnvKeys { static const kWinAppName = 'CW_WIN_APP_NAME'; diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 36661ac7e..43386494e 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -1,5 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; @@ -37,6 +39,14 @@ abstract class WalletCreationVMBase with Store { @observable ExecutionState state; + @observable + String? walletPassword; + + @observable + String? repeatedWalletPassword; + + bool get hasWalletPassword => SettingsStoreBase.walletPasswordDirectInput; + WalletType type; final bool isRecovery; final WalletCreationService walletCreationService; @@ -59,6 +69,14 @@ abstract class WalletCreationVMBase with Store { name = await generateName(); } + if (hasWalletPassword && (walletPassword?.isEmpty ?? true)) { + throw Exception(S.current.wallet_password_is_empty); + } + + if (hasWalletPassword && walletPassword != repeatedWalletPassword) { + throw Exception(S.current.repeated_password_is_incorrect); + } + walletCreationService.checkIfExists(name); final dirPath = await pathForWalletDir(name: name, type: type); final path = await pathForWallet(name: name, type: type); diff --git a/lib/view_model/wallet_list/wallet_edit_view_model.dart b/lib/view_model/wallet_list/wallet_edit_view_model.dart index 0582c3f87..e5bfcd4e3 100644 --- a/lib/view_model/wallet_list/wallet_edit_view_model.dart +++ b/lib/view_model/wallet_list/wallet_edit_view_model.dart @@ -32,10 +32,11 @@ abstract class WalletEditViewModelBase with Store { final WalletLoadingService _walletLoadingService; @action - Future changeName(WalletListItem walletItem) async { + Future changeName(WalletListItem walletItem, {String? password}) async { state = WalletEditRenamePending(); await _walletLoadingService.renameWallet( - walletItem.type, walletItem.name, newName); + walletItem.type, walletItem.name, newName, + password: password); _walletListViewModel.updateList(); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 4729a38b2..a618695b1 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -66,25 +66,25 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { switch (type) { case WalletType.monero: return monero!.createMoneroNewWalletCredentials( - name: name, language: options!.first as String, isPolyseed: options.last as bool); + name: name, language: options!.first as String, password: walletPassword, isPolyseed: options.last as bool); case WalletType.bitcoin: - return bitcoin!.createBitcoinNewWalletCredentials(name: name); + return bitcoin!.createBitcoinNewWalletCredentials(name: name, password: walletPassword); case WalletType.litecoin: - return bitcoin!.createBitcoinNewWalletCredentials(name: name); + return bitcoin!.createBitcoinNewWalletCredentials(name: name, password: walletPassword); case WalletType.haven: - return haven! - .createHavenNewWalletCredentials(name: name, language: options!.first as String); + return haven!.createHavenNewWalletCredentials( + name: name, language: options!.first as String, password: walletPassword); case WalletType.ethereum: - return ethereum!.createEthereumNewWalletCredentials(name: name); + return ethereum!.createEthereumNewWalletCredentials(name: name, password: walletPassword); case WalletType.bitcoinCash: - return bitcoinCash!.createBitcoinCashNewWalletCredentials(name: name); + return bitcoinCash!.createBitcoinCashNewWalletCredentials(name: name, password: walletPassword); case WalletType.nano: case WalletType.banano: return nano!.createNanoNewWalletCredentials(name: name); case WalletType.polygon: - return polygon!.createPolygonNewWalletCredentials(name: name); + return polygon!.createPolygonNewWalletCredentials(name: name, password: walletPassword); case WalletType.solana: - return solana!.createSolanaNewWalletCredentials(name: name); + return solana!.createSolanaNewWalletCredentials(name: name, password: walletPassword); case WalletType.tron: return tron!.createTronNewWalletCredentials(name: name); case WalletType.wownero: diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 25a555b44..a38baabd8 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -86,7 +86,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { @override WalletCredentials getCredentials(dynamic options) { - final password = generateWalletPassword(); + final password = walletPassword ?? generateWalletPassword(); String? passphrase = options['passphrase'] as String?; final height = options['height'] as int? ?? 0; name = options['name'] as String; diff --git a/lib/view_model/wallet_unlock_loadable_view_model.dart b/lib/view_model/wallet_unlock_loadable_view_model.dart new file mode 100644 index 000000000..f5e7bb917 --- /dev/null +++ b/lib/view_model/wallet_unlock_loadable_view_model.dart @@ -0,0 +1,63 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/view_model/wallet_unlock_view_model.dart'; + +part 'wallet_unlock_loadable_view_model.g.dart'; + +class WalletUnlockLoadableViewModel = WalletUnlockLoadableViewModelBase + with _$WalletUnlockLoadableViewModel; + +abstract class WalletUnlockLoadableViewModelBase extends WalletUnlockViewModel with Store { + WalletUnlockLoadableViewModelBase(this._appStore, this._walletLoadingService, + {required this.walletName, required this.walletType}) + : password = '', + state = InitialExecutionState(); + + final String walletName; + + final WalletType walletType; + + @override + @observable + String password; + + @override + @observable + ExecutionState state; + + final WalletLoadingService _walletLoadingService; + + final AppStore _appStore; + + @override + @action + void setPassword(String password) => this.password = password; + + @override + @action + Future unlock() async { + try { + state = InitialExecutionState(); + final wallet = await _walletLoadingService.load(walletType, walletName, password: password); + _appStore.changeCurrentWallet(wallet); + success(); + } catch (e) { + failure(e.toString()); + } + } + + @override + @action + void success() { + state = ExecutedSuccessfullyState(); + } + + @override + @action + void failure(e) { + state = FailureState(e.toString()); + } +} diff --git a/lib/view_model/wallet_unlock_verifiable_view_model.dart b/lib/view_model/wallet_unlock_verifiable_view_model.dart new file mode 100644 index 000000000..d6f5f0e7d --- /dev/null +++ b/lib/view_model/wallet_unlock_verifiable_view_model.dart @@ -0,0 +1,60 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/view_model/wallet_unlock_view_model.dart'; + +part 'wallet_unlock_verifiable_view_model.g.dart'; + +class WalletUnlockVerifiableViewModel = WalletUnlockVerifiableViewModelBase + with _$WalletUnlockVerifiableViewModel; + +abstract class WalletUnlockVerifiableViewModelBase extends WalletUnlockViewModel with Store { + WalletUnlockVerifiableViewModelBase(this.appStore, + {required this.walletName, required this.walletType}) + : password = '', + state = InitialExecutionState(); + + final String walletName; + + final WalletType walletType; + + final AppStore appStore; + + @override + @observable + String password; + + @override + @observable + ExecutionState state; + + @override + @action + void setPassword(String password) => this.password = password; + + @override + @action + Future unlock() async { + try { + state = appStore.wallet!.password == password + ? ExecutedSuccessfullyState() + : FailureState(S.current.invalid_password); + } catch (e) { + failure('${S.current.invalid_password}\n${e.toString()}'); + } + } + + @override + @action + void success() { + state = ExecutedSuccessfullyState(); + } + + @override + @action + void failure(e) { + state = FailureState(e.toString()); + } +} diff --git a/lib/view_model/wallet_unlock_view_model.dart b/lib/view_model/wallet_unlock_view_model.dart new file mode 100644 index 000000000..f0131c61f --- /dev/null +++ b/lib/view_model/wallet_unlock_view_model.dart @@ -0,0 +1,11 @@ +import 'package:cake_wallet/core/execution_state.dart'; + +abstract class WalletUnlockViewModel { + String get walletName; + String get password; + void setPassword(String password); + ExecutionState get state; + Future unlock(); + void success(); + void failure(dynamic e); +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 000000000..bfce34c34 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,144 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "cake_wallet") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.cake_wallet") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-linux-gnu_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "monero_libwallet2_api_c.so" + COMPONENT Runtime) + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/wownero/x86_64-linux-gnu_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "wownero_libwallet2_api_c.so" + COMPONENT Runtime) + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/com.cakewallet.CakeWallet.desktop b/linux/com.cakewallet.CakeWallet.desktop new file mode 100644 index 000000000..eb76a2fb5 --- /dev/null +++ b/linux/com.cakewallet.CakeWallet.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Cake Wallet +Comment=A noncustodial multi-currency wallet +Categories=Office;Finance; +Terminal=false +Icon=com.cakewallet.CakeWallet +Exec=cake_wallet diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..d5bd01648 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..01b922894 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,27 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) devicelocale_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DevicelocalePlugin"); + devicelocale_plugin_register_with_registrar(devicelocale_registrar); + g_autoptr(FlPluginRegistrar) flutter_local_authentication_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalAuthenticationPlugin"); + flutter_local_authentication_plugin_register_with_registrar(flutter_local_authentication_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..f52be7481 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + devicelocale + flutter_local_authentication + flutter_secure_storage_linux + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + sp_scanner +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 000000000..e7c5c5437 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 000000000..7375d05ca --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "cake_wallet"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "cake_wallet"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 000000000..72271d5e4 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/CakeWallet/decrypt.swift b/macos/CakeWallet/decrypt.swift new file mode 100644 index 000000000..5f24ad3fb --- /dev/null +++ b/macos/CakeWallet/decrypt.swift @@ -0,0 +1,16 @@ +import Foundation +import CryptoSwift + +func decrypt(data: Data, key: String, salt: String) -> String? { + let keyBytes = key.data(using: .utf8)?.bytes ?? [] + let saltBytes = salt.data(using: .utf8)?.bytes ?? [] + + guard let PBKDF2key = try? PKCS5.PBKDF2(password: keyBytes, salt: saltBytes, iterations: 4096, variant: .sha256).calculate(), + let cipher = try? Blowfish(key: PBKDF2key, padding: .pkcs7), + let decryptedBytes = try? cipher.decrypt(data.bytes) else { + return nil + } + + let decryptedData = Data(decryptedBytes) + return String(data: decryptedData, encoding: .utf8) +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 338ece4ce..0b4ee9415 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,7 +12,6 @@ import flutter_inappwebview_macos import flutter_local_authentication import flutter_secure_storage_macos import in_app_review -import package_info import package_info_plus import path_provider_foundation import share_plus @@ -28,7 +27,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalAuthenticationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalAuthenticationPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) - FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/macos/Runner/RunnerBase.entitlements b/macos/Runner/RunnerBase.entitlements index 186b61b17..22be818c6 100644 --- a/macos/Runner/RunnerBase.entitlements +++ b/macos/Runner/RunnerBase.entitlements @@ -11,4 +11,4 @@ $(AppIdentifierPrefix)${BUNDLE_ID} - + \ No newline at end of file diff --git a/model_generator.sh b/model_generator.sh index 58ce9b5d0..8a1173f7d 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,3 +1,4 @@ +#!/bin/bash cd cw_core; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. cd cw_evm; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. cd cw_monero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 90072a7c1..567d1b210 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -31,8 +31,7 @@ dependencies: flutter_local_authentication: git: url: https://github.com/cake-tech/flutter_local_authentication - package_info: ^2.0.0 - #package_info_plus: ^1.4.2 + package_info_plus: ^8.0.1 devicelocale: git: url: https://github.com/cake-tech/flutter-devicelocale @@ -80,6 +79,7 @@ dependencies: path_provider_android: ^2.2.1 shared_preferences_android: 2.0.17 url_launcher_android: 6.0.24 + url_launcher_linux: 3.1.1 # https://github.com/flutter/flutter/issues/153083 sensitive_clipboard: ^1.0.0 walletconnect_flutter_v2: ^2.1.4 eth_sig_util: ^0.0.9 diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 4cf9509f8..98cafdebc 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -236,6 +236,7 @@ "enter_code": "ادخل الرمز", "enter_seed_phrase": "أدخل عبارة البذور الخاصة بك", "enter_totp_code": "الرجاء إدخال رمز TOTP.", + "enter_wallet_password": "أدخل كلمة مرور المحفظة", "enter_your_note": "أدخل ملاحظتك ...", "enter_your_pin": "أدخل كود الرقم السري", "enter_your_pin_again": "أدخل PIN الخاص بك مرة أخرى", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "ليس لديك ما يكفي من SOL لتغطية رسوم المعاملة والإيجار للحساب. يرجى إضافة المزيد من sol إلى محفظتك أو تقليل مبلغ sol الذي ترسله", "introducing_cake_pay": "نقدم لكم Cake Pay!", "invalid_input": "مدخل غير صالح", + "invalid_password": "رمز مرور خاطئ", "invoice_details": "تفاصيل الفاتورة", "is_percentage": "يكون", "last_30_days": "آخر 30 يومًا", @@ -499,6 +501,8 @@ "rename": "إعادة تسمية", "rep_warning": "تحذير تمثيلي", "rep_warning_sub": "لا يبدو أن ممثلك في وضع جيد. اضغط هنا لاختيار واحدة جديدة", + "repeat_wallet_password": "كرر كلمة مرور المحفظة", + "repeated_password_is_incorrect": "كلمة المرور المتكررة غير صحيحة. يرجى تكرار كلمة مرور المحفظة مرة أخرى.", "require_for_adding_contacts": "تتطلب إضافة جهات اتصال", "require_for_all_security_and_backup_settings": "مطلوب لجميع إعدادات الأمان والنسخ الاحتياطي", "require_for_assessing_wallet": "تتطلب الوصول إلى المحفظة", @@ -797,6 +801,7 @@ "unavailable_balance_description": ".ﺎﻫﺪﻴﻤﺠﺗ ءﺎﻐﻟﺇ ﺭﺮﻘﺗ ﻰﺘﺣ ﺕﻼﻣﺎﻌﻤﻠﻟ ﻝﻮﺻﻮﻠﻟ ﺔﻠﺑﺎﻗ ﺮﻴﻏ ﺓﺪﻤﺠﻤﻟﺍ ﺓﺪﺻﺭﻷﺍ ﻞﻈﺗ ﺎﻤﻨﻴﺑ ،ﺎﻬﺑ ﺔﺻﺎﺨﻟﺍ ﺕﻼﻣﺎﻌﻤﻟﺍ ﻝﺎﻤﺘﻛﺍ ﺩﺮﺠﻤﺑ ﺔﺣﺎﺘﻣ ﺔﻠﻔﻘﻤﻟﺍ ﺓﺪﺻﺭﻷﺍ ﺢﺒﺼﺘﺳ .ﻚﺑ ﺔﺻﺎﺨﻟﺍ ﺕﻼﻤﻌﻟﺍ ﻲﻓ ﻢﻜﺤﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻂﺸﻧ ﻞﻜﺸﺑ ﺎﻫﺪﻴﻤﺠﺘﺑ ﺖﻤﻗ", "unconfirmed": "رصيد غير مؤكد", "understand": "لقد فهمت", + "unlock": "الغاء القفل", "unmatched_currencies": "عملة محفظتك الحالية لا تتطابق مع عملة QR الممسوحة ضوئيًا", "unspent_change": "يتغير", "unspent_coins_details_title": "تفاصيل العملات الغير المنفقة", @@ -838,6 +843,7 @@ "wallet_menu": "قائمة", "wallet_name": "اسم المحفظة", "wallet_name_exists": "توجد بالفعل محفظة بهذا الاسم. الرجاء اختيار اسم مختلف أو إعادة تسمية المحفظة الأخرى أولاً.", + "wallet_password_is_empty": "كلمة مرور المحفظة فارغة. يجب ألا تكون كلمة مرور المحفظة فارغة", "wallet_recovery_height": "ارتفاع الاسترداد", "wallet_restoration_store_incorrect_seed_length": "طول السييد غير صحيح", "wallet_seed": "سييد المحفظة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index b76401cf4..b51167dab 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -236,6 +236,7 @@ "enter_code": "Въведете код", "enter_seed_phrase": "Въведете вашата фраза за семена", "enter_totp_code": "Моля, въведете TOTP кода.", + "enter_wallet_password": "Въведете паролата за портфейла", "enter_your_note": "Въвеждане на бележка…", "enter_your_pin": "Въведете PIN", "enter_your_pin_again": "Въведете своя PIN отново", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Нямате достатъчно SOL, за да покриете таксата за транзакцията и наемането на сметката. Моля, добавете повече SOL към портфейла си или намалете сумата на SOL, която изпращате", "introducing_cake_pay": "Запознайте се с Cake Pay!", "invalid_input": "Невалиден вход", + "invalid_password": "Невалидна парола", "invoice_details": "IДанни за фактура", "is_percentage": "е", "last_30_days": "Последните 30 дни", @@ -499,6 +501,8 @@ "rename": "Промяна на името", "rep_warning": "Представително предупреждение", "rep_warning_sub": "Вашият представител изглежда не е в добро състояние. Докоснете тук, за да изберете нов", + "repeat_wallet_password": "Повторете паролата на портфейла", + "repeated_password_is_incorrect": "Многократната парола е неправилна. Моля, повторете отново паролата за портфейла.", "require_for_adding_contacts": "Изисква се за добавяне на контакти", "require_for_all_security_and_backup_settings": "Изисква се за всички настройки за сигурност и архивиране", "require_for_assessing_wallet": "Изискване за достъп до портфейла", @@ -797,6 +801,7 @@ "unavailable_balance_description": "Неналично салдо: Тази обща сума включва средства, които са заключени в чакащи транзакции и тези, които сте замразили активно в настройките за контрол на монетите. Заключените баланси ще станат достъпни, след като съответните им транзакции бъдат завършени, докато замразените баланси остават недостъпни за транзакции, докато не решите да ги размразите.", "unconfirmed": "Непотвърден баланс", "understand": "Разбирам", + "unlock": "Отключване", "unmatched_currencies": "Валутата на този портфейл не съвпада с тази от сканирания QR код", "unspent_change": "Промяна", "unspent_coins_details_title": "Подробности за неизползваните монети", @@ -838,6 +843,7 @@ "wallet_menu": "Меню", "wallet_name": "Име на портфейл", "wallet_name_exists": "Вече има портфейл с това име. Моля, изберете друго име или преименувайте другия портфейл.", + "wallet_password_is_empty": "Паролата за портфейл е празна. Паролата за портфейл не трябва да е празна", "wallet_recovery_height": "Височина на възстановяване", "wallet_restoration_store_incorrect_seed_length": "Грешна дължина на seed-а", "wallet_seed": "Seed на портфейла", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index c5d374dd0..d47ca932c 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -236,6 +236,7 @@ "enter_code": "Zadejte kód", "enter_seed_phrase": "Zadejte svou frázi semen", "enter_totp_code": "Zadejte kód TOTP.", + "enter_wallet_password": "Zadejte heslo peněženky", "enter_your_note": "Zadejte poznámku…", "enter_your_pin": "Zadejte svůj PIN", "enter_your_pin_again": "Zadejte znovu svůj PIN", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Nemáte dostatek SOL na pokrytí transakčního poplatku a nájemného za účet. Laskavě přidejte do své peněženky více SOL nebo snižte množství Sol, kterou odesíláte", "introducing_cake_pay": "Představujeme Cake Pay!", "invalid_input": "Neplatný vstup", + "invalid_password": "Neplatné heslo", "invoice_details": "detaily faktury", "is_percentage": "je", "last_30_days": "Posledních 30 dnů", @@ -499,6 +501,8 @@ "rename": "Přejmenovat", "rep_warning": "Reprezentativní varování", "rep_warning_sub": "Zdá se, že váš zástupce není v dobrém stavu. Klepnutím zde vyberte nový", + "repeat_wallet_password": "Opakujte heslo peněženky", + "repeated_password_is_incorrect": "Opakované heslo je nesprávné. Znovu opakujte heslo peněženky.", "require_for_adding_contacts": "Vyžadovat pro přidání kontaktů", "require_for_all_security_and_backup_settings": "Vyžadovat všechna nastavení zabezpečení a zálohování", "require_for_assessing_wallet": "Vyžadovat pro přístup k peněžence", @@ -797,6 +801,7 @@ "unavailable_balance_description": "Nedostupný zůstatek: Tento součet zahrnuje prostředky, které jsou uzamčeny v nevyřízených transakcích a ty, které jste aktivně zmrazili v nastavení kontroly mincí. Uzamčené zůstatky budou k dispozici po dokončení příslušných transakcí, zatímco zmrazené zůstatky zůstanou pro transakce nepřístupné, dokud se nerozhodnete je uvolnit.", "unconfirmed": "Nepotvrzený zůstatek", "understand": "Rozumím", + "unlock": "Odemknout", "unmatched_currencies": "Měna vaší současné peněženky neodpovídá té v naskenovaném QR kódu", "unspent_change": "Změna", "unspent_coins_details_title": "Podrobnosti o neutracených mincích", @@ -838,6 +843,7 @@ "wallet_menu": "Menu", "wallet_name": "Název peněženky", "wallet_name_exists": "Peněženka s tímto názvem už existuje. Prosím zvolte si jiný název, nebo nejprve přejmenujte nejprve druhou peněženku.", + "wallet_password_is_empty": "Heslo peněženky je prázdné. Heslo peněženky by nemělo být prázdné", "wallet_recovery_height": "Výška zotavení", "wallet_restoration_store_incorrect_seed_length": "Nesprávná délka seedu", "wallet_seed": "Seed peněženky", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 7b6613dd6..e64c3bc27 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -236,6 +236,7 @@ "enter_code": "Code eingeben", "enter_seed_phrase": "Geben Sie Ihre Seed-Phrase ein", "enter_totp_code": "Bitte geben Sie den TOTP-Code ein.", + "enter_wallet_password": "Geben Sie das Brieftaschenkennwort ein", "enter_your_note": "Geben Sie Ihre Bemerkung ein…", "enter_your_pin": "PIN eingeben", "enter_your_pin_again": "Geben Sie Ihre PIN erneut ein", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Brieftasche hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", "introducing_cake_pay": "Einführung von Cake Pay!", "invalid_input": "Ungültige Eingabe", + "invalid_password": "Ungültiges Passwort", "invoice_details": "Rechnungs-Details", "is_percentage": "ist", "last_30_days": "Letzte 30 Tage", @@ -500,6 +502,8 @@ "rename": "Umbenennen", "rep_warning": "Repräsentative Warnung", "rep_warning_sub": "Ihr Vertreter scheint nicht gut zu sein. Tippen Sie hier, um eine neue auszuwählen", + "repeat_wallet_password": "Wiederholen Sie das Brieftaschenkennwort", + "repeated_password_is_incorrect": "Wiederholtes Passwort ist falsch. Bitte wiederholen Sie das Brieftaschenkennwort erneut.", "require_for_adding_contacts": "Erforderlich zum Hinzufügen von Kontakten", "require_for_all_security_and_backup_settings": "Für alle Sicherheits- und Sicherungseinstellungen erforderlich", "require_for_assessing_wallet": "Für den Zugriff auf die Wallet erforderlich", @@ -799,6 +803,7 @@ "unconfirmed": "Unbestätigter Saldo", "und": "und", "understand": "Ich verstehe", + "unlock": "Freischalten", "unmatched_currencies": "Die Währung Ihres aktuellen Wallets stimmt nicht mit der des gescannten QR überein", "unspent_change": "Wechselgeld", "unspent_coins_details_title": "Details zu nicht ausgegebenen Coins", @@ -841,6 +846,7 @@ "wallet_menu": "Wallet-Menü", "wallet_name": "Walletname", "wallet_name_exists": "Wallet mit diesem Namen existiert bereits", + "wallet_password_is_empty": "Brieftaschenkennwort ist leer. Brieftaschenkennwort sollte nicht leer sein", "wallet_recovery_height": "Erstellungshöhe", "wallet_restoration_store_incorrect_seed_length": "Falsche Seed-Länge", "wallet_seed": "Wallet-Seed", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 35712a780..9d7d9ad49 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -236,6 +236,7 @@ "enter_code": "Enter code", "enter_seed_phrase": "Enter your seed phrase", "enter_totp_code": "Please enter the TOTP Code.", + "enter_wallet_password": "Enter the wallet password", "enter_your_note": "Enter your note…", "enter_your_pin": "Enter your PIN", "enter_your_pin_again": "Enter your pin again", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "You do not have enough SOL to cover the transaction fee and rent for the account. Kindly add more SOL to your wallet or reduce the SOL amount you\\'re sending", "introducing_cake_pay": "Introducing Cake Pay!", "invalid_input": "Invalid input", + "invalid_password": "Invalid password", "invoice_details": "Invoice details", "is_percentage": "is", "last_30_days": "Last 30 days", @@ -499,6 +501,8 @@ "rename": "Rename", "rep_warning": "Representative Warning", "rep_warning_sub": "Your representative does not appear to be in good standing. Tap here to select a new one", + "repeat_wallet_password": "Repeat the wallet password", + "repeated_password_is_incorrect": "Repeated password is incorrect. Please repeat the wallet password again.", "require_for_adding_contacts": "Require for adding contacts", "require_for_all_security_and_backup_settings": "Require for all security and backup settings", "require_for_assessing_wallet": "Require for accessing wallet", @@ -798,6 +802,7 @@ "unavailable_balance_description": "Unavailable Balance: This total includes funds that are locked in pending transactions and those you have actively frozen in your coin control settings. Locked balances will become available once their respective transactions are completed, while frozen balances remain inaccessible for transactions until you decide to unfreeze them.", "unconfirmed": "Unconfirmed Balance", "understand": "I understand", + "unlock": "Unlock", "unmatched_currencies": "Your current wallet's currency does not match that of the scanned QR", "unspent_change": "Change", "unspent_coins_details_title": "Unspent coins details", @@ -839,6 +844,7 @@ "wallet_menu": "Menu", "wallet_name": "Wallet name", "wallet_name_exists": "A wallet with that name already exists. Please choose a different name or rename the other wallet first.", + "wallet_password_is_empty": "Wallet password is empty. Wallet password should not be empty", "wallet_recovery_height": "Recovery Height", "wallet_restoration_store_incorrect_seed_length": "Incorrect seed length", "wallet_seed": "Wallet seed", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 31a6e6865..e7f1366a5 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -236,6 +236,7 @@ "enter_code": "Ingresar código", "enter_seed_phrase": "Ingrese su frase de semillas", "enter_totp_code": "Ingrese el código TOTP.", + "enter_wallet_password": "Ingrese la contraseña de la billetera", "enter_your_note": "Ingresa tu nota…", "enter_your_pin": "Introduce tu PIN", "enter_your_pin_again": "Ingrese su PIN nuevamente", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "No tiene suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando", "introducing_cake_pay": "¡Presentamos Cake Pay!", "invalid_input": "Entrada inválida", + "invalid_password": "Contraseña invalida", "invoice_details": "Detalles de la factura", "is_percentage": "es", "last_30_days": "Últimos 30 días", @@ -500,6 +502,8 @@ "rename": "Rebautizar", "rep_warning": "Advertencia representativa", "rep_warning_sub": "Su representante no parece estar en buena posición. Toque aquí para seleccionar uno nuevo", + "repeat_wallet_password": "Repita la contraseña de billetera", + "repeated_password_is_incorrect": "La contraseña repetida es incorrecta. Repita la contraseña de la billetera nuevamente.", "require_for_adding_contacts": "Requerido para agregar contactos", "require_for_all_security_and_backup_settings": "Requerido para todas las configuraciones de seguridad y copia de seguridad", "require_for_assessing_wallet": "Requerido para acceder a la billetera", @@ -798,6 +802,7 @@ "unavailable_balance_description": "Saldo no disponible: este total incluye fondos que están bloqueados en transacciones pendientes y aquellos que usted ha congelado activamente en su configuración de control de monedas. Los saldos bloqueados estarán disponibles una vez que se completen sus respectivas transacciones, mientras que los saldos congelados permanecerán inaccesibles para las transacciones hasta que usted decida descongelarlos.", "unconfirmed": "Saldo no confirmado", "understand": "Entiendo", + "unlock": "desbloquear", "unmatched_currencies": "La moneda de su billetera actual no coincide con la del QR escaneado", "unspent_change": "Cambiar", "unspent_coins_details_title": "Detalles de monedas no gastadas", @@ -839,6 +844,7 @@ "wallet_menu": "Menú de billetera", "wallet_name": "Nombre de la billetera", "wallet_name_exists": "Wallet con ese nombre ya ha existido", + "wallet_password_is_empty": "La contraseña de billetera está vacía. La contraseña de la billetera no debe estar vacía", "wallet_recovery_height": "Altura de recuperación", "wallet_restoration_store_incorrect_seed_length": "Longitud de semilla incorrecta", "wallet_seed": "Semilla de billetera", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 03d4a73dd..7965eca12 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -236,6 +236,7 @@ "enter_code": "Entrez le code", "enter_seed_phrase": "Entrez votre phrase secrète (seed)", "enter_totp_code": "Veuillez entrer le code TOTP.", + "enter_wallet_password": "Entrez le mot de passe du portefeuille", "enter_your_note": "Entrez votre note…", "enter_your_pin": "Entrez votre code PIN", "enter_your_pin_again": "Entrez à nouveau votre code PIN", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Vous n'avez pas assez de SOL pour couvrir les frais de transaction et le loyer pour le compte. Veuillez ajouter plus de Sol à votre portefeuille ou réduire la quantité de sol que vous envoyez", "introducing_cake_pay": "Présentation de Cake Pay !", "invalid_input": "Entrée invalide", + "invalid_password": "Mot de passe incorrect", "invoice_details": "Détails de la facture", "is_percentage": "est", "last_30_days": "30 derniers jours", @@ -499,6 +501,8 @@ "rename": "Renommer", "rep_warning": "Avertissement représentatif", "rep_warning_sub": "Votre représentant ne semble pas être en règle. Appuyez ici pour en sélectionner un nouveau", + "repeat_wallet_password": "Répétez le mot de passe du portefeuille", + "repeated_password_is_incorrect": "Le mot de passe répété est incorrect. Veuillez répéter le mot de passe du portefeuille.", "require_for_adding_contacts": "Requis pour ajouter des contacts", "require_for_all_security_and_backup_settings": "Exiger pour tous les paramètres de sécurité et de sauvegarde", "require_for_assessing_wallet": "Nécessaire pour accéder au portefeuille", @@ -797,6 +801,7 @@ "unavailable_balance_description": "Solde indisponible : ce total comprend les fonds bloqués dans les transactions en attente et ceux que vous avez activement gelés dans vos paramètres de contrôle des pièces. Les soldes bloqués deviendront disponibles une fois leurs transactions respectives terminées, tandis que les soldes gelés resteront inaccessibles aux transactions jusqu'à ce que vous décidiez de les débloquer.", "unconfirmed": "Solde non confirmé", "understand": "J'ai compris", + "unlock": "Ouvrir", "unmatched_currencies": "La devise de votre portefeuille (wallet) actuel ne correspond pas à celle du QR code scanné", "unspent_change": "Monnaie", "unspent_coins_details_title": "Détails des pièces (coins) non dépensées", @@ -838,6 +843,7 @@ "wallet_menu": "Menu", "wallet_name": "Nom du Portefeuille (Wallet)", "wallet_name_exists": "Un portefeuille (wallet) portant ce nom existe déjà", + "wallet_password_is_empty": "Le mot de passe du portefeuille est vide. Le mot de passe du portefeuille ne doit pas être vide", "wallet_recovery_height": "Hauteur de récupération", "wallet_restoration_store_incorrect_seed_length": "Longueur de phrase secrète (seed) incorrecte", "wallet_seed": "Phrase secrète (seed) du portefeuille (wallet)", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 922f9a51b..d2c58971f 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -236,6 +236,7 @@ "enter_code": "Shigar da lamba", "enter_seed_phrase": "Shigar da Sert Sentarku", "enter_totp_code": "Da fatan za a shigar da lambar tnp.", + "enter_wallet_password": "Shigar da kalmar sirri ta walat", "enter_your_note": "Shigar da bayanin kula…", "enter_your_pin": "Shigar da PIN", "enter_your_pin_again": "Shigar da PIN ɗinku na sake", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Ba ku da isasshen Sol don rufe kuɗin ma'amala da haya don asusun. Da kyau ƙara ƙarin sool zuwa walat ɗinku ko rage adadin Sol ɗin da kuke aikawa", "introducing_cake_pay": "Gabatar da Cake Pay!", "invalid_input": "Shigar da ba daidai ba", + "invalid_password": "Kalmar sirri mara inganci", "invoice_details": "Bayanin wadannan", "is_percentage": "shine", "last_30_days": "Kwanaki 30 na ƙarshe", @@ -501,6 +503,8 @@ "rename": "Sake suna", "rep_warning": "Gargadi Wakilin", "rep_warning_sub": "Wakilinku bai bayyana ya kasance cikin kyakkyawan yanayi ba. Matsa nan don zaɓar sabon", + "repeat_wallet_password": "Maimaita kalmar sirri", + "repeated_password_is_incorrect": "Maimaita kalmar sirri ba daidai ba ce. Da fatan za a sake maimaita kalmar sirri.", "require_for_adding_contacts": "Bukatar ƙara lambobin sadarwa", "require_for_all_security_and_backup_settings": "Bukatar duk tsaro da saitunan wariyar ajiya", "require_for_assessing_wallet": "Bukatar samun damar walat", @@ -799,6 +803,7 @@ "unavailable_balance_description": "Ma'auni Babu: Wannan jimlar ya haɗa da kuɗi waɗanda ke kulle a cikin ma'amaloli da ke jiran aiki da waɗanda kuka daskare sosai a cikin saitunan sarrafa kuɗin ku. Ma'auni da aka kulle za su kasance da zarar an kammala ma'amalolinsu, yayin da daskararrun ma'auni ba za su iya samun damar yin ciniki ba har sai kun yanke shawarar cire su.", "unconfirmed": "Ba a tabbatar ba", "understand": "na gane", + "unlock": "Buɗe", "unmatched_currencies": "Nau'in walat ɗin ku na yanzu bai dace da na lambar QR da aka bincika ba", "unspent_change": "Canza", "unspent_coins_details_title": "Bayanan tsabar kudi da ba a kashe ba", @@ -840,6 +845,7 @@ "wallet_menu": "Menu", "wallet_name": "Sunan walat", "wallet_name_exists": "Wallet mai wannan sunan ya riga ya wanzu. Da fatan za a zaɓi wani suna daban ko sake suna ɗayan walat tukuna.", + "wallet_password_is_empty": "Alamar Wallet babu komai. Al'adun Wallet bai zama komai ba", "wallet_recovery_height": "Mai Tsaro", "wallet_restoration_store_incorrect_seed_length": "kalmar sirrin iri ba daidai ba", "wallet_seed": "kalmar sirri na walat", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index db6940e8b..baa3925c0 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -236,6 +236,7 @@ "enter_code": "कोड दर्ज करें", "enter_seed_phrase": "अपना बीज वाक्यांश दर्ज करें", "enter_totp_code": "कृपया TOTP कोड दर्ज करें।", + "enter_wallet_password": "वॉलेट पासवर्ड दर्ज करें", "enter_your_note": "अपना नोट दर्ज करें ...", "enter_your_pin": "अपना पिन दर्ज करो", "enter_your_pin_again": "फिर से अपना पिन डालें", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "आपके पास लेन -देन शुल्क और खाते के लिए किराए को कवर करने के लिए पर्याप्त सोल नहीं है। कृपया अपने बटुए में अधिक सोल जोड़ें या सोल राशि को कम करें जिसे आप भेज रहे हैं", "introducing_cake_pay": "परिचय Cake Pay!", "invalid_input": "अमान्य निवेश", + "invalid_password": "अवैध पासवर्ड", "invoice_details": "चालान विवरण", "is_percentage": "है", "last_30_days": "पिछले 30 दिन", @@ -501,6 +503,8 @@ "rename": "नाम बदलें", "rep_warning": "प्रतिनिधि चेतावनी", "rep_warning_sub": "आपका प्रतिनिधि अच्छी स्थिति में नहीं दिखाई देता है। एक नया चयन करने के लिए यहां टैप करें", + "repeat_wallet_password": "वॉलेट पासवर्ड दोहराएं", + "repeated_password_is_incorrect": "बार -बार पासवर्ड गलत है। कृपया फिर से वॉलेट पासवर्ड दोहराएं।", "require_for_adding_contacts": "संपर्क जोड़ने के लिए आवश्यकता है", "require_for_all_security_and_backup_settings": "सभी सुरक्षा और बैकअप सेटिंग्स की आवश्यकता है", "require_for_assessing_wallet": "वॉलेट तक पहुँचने के लिए आवश्यकता है", @@ -799,6 +803,7 @@ "unavailable_balance_description": "अनुपलब्ध शेष राशि: इस कुल में वे धनराशि शामिल हैं जो लंबित लेनदेन में बंद हैं और जिन्हें आपने अपनी सिक्का नियंत्रण सेटिंग्स में सक्रिय रूप से जमा कर रखा है। लॉक किए गए शेष उनके संबंधित लेन-देन पूरे होने के बाद उपलब्ध हो जाएंगे, जबकि जमे हुए शेष लेन-देन के लिए अप्राप्य रहेंगे जब तक कि आप उन्हें अनफ्रीज करने का निर्णय नहीं लेते।", "unconfirmed": "अपुष्ट शेष राशि", "understand": "मुझे समझ", + "unlock": "अनलॉक", "unmatched_currencies": "आपके वर्तमान वॉलेट की मुद्रा स्कैन किए गए क्यूआर से मेल नहीं खाती", "unspent_change": "परिवर्तन", "unspent_coins_details_title": "अव्ययित सिक्कों का विवरण", @@ -840,6 +845,7 @@ "wallet_menu": "बटुआ मेनू", "wallet_name": "बटुए का नाम", "wallet_name_exists": "उस नाम वाला वॉलेट पहले से मौजूद है", + "wallet_password_is_empty": "वॉलेट पासवर्ड खाली है। वॉलेट पासवर्ड खाली नहीं होना चाहिए", "wallet_recovery_height": "वसूली ऊंचाई", "wallet_restoration_store_incorrect_seed_length": "गलत बीज की लंबाई", "wallet_seed": "बटुआ का बीज", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 57cf1361e..e6b820300 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -236,6 +236,7 @@ "enter_code": "Unesite kod", "enter_seed_phrase": "Unesite svoju sjemensku frazu", "enter_totp_code": "Unesite TOTP kod.", + "enter_wallet_password": "Unesite lozinku za novčanik", "enter_your_note": "Unesite svoju poruku…", "enter_your_pin": "Upišite PIN", "enter_your_pin_again": "Ponovno upišite pin", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Nemate dovoljno SOL -a za pokrivanje naknade za transakciju i najamninu za račun. Ljubazno dodajte više sol u svoj novčanik ili smanjite količinu SOL -a koju šaljete", "introducing_cake_pay": "Predstavljamo Cake Pay!", "invalid_input": "Pogrešan unos", + "invalid_password": "Netočna zaporka", "invoice_details": "Podaci o fakturi", "is_percentage": "je", "last_30_days": "Zadnjih 30 dana", @@ -499,6 +501,8 @@ "rename": "Preimenuj", "rep_warning": "Reprezentativno upozorenje", "rep_warning_sub": "Čini se da vaš predstavnik nije u dobrom stanju. Dodirnite ovdje za odabir novog", + "repeat_wallet_password": "Ponovite lozinku za novčanik", + "repeated_password_is_incorrect": "Ponovljena lozinka je netočna. Molimo ponovite lozinku za novčanik.", "require_for_adding_contacts": "Zahtijeva za dodavanje kontakata", "require_for_all_security_and_backup_settings": "Zahtijeva za sve postavke sigurnosti i sigurnosne kopije", "require_for_assessing_wallet": "Potreban za pristup novčaniku", @@ -797,6 +801,7 @@ "unavailable_balance_description": "Nedostupno stanje: Ovaj ukupni iznos uključuje sredstva koja su zaključana u transakcijama na čekanju i ona koja ste aktivno zamrznuli u postavkama kontrole novčića. Zaključani saldi postat će dostupni kada se dovrše njihove transakcije, dok zamrznuti saldi ostaju nedostupni za transakcije sve dok ih ne odlučite odmrznuti.", "unconfirmed": "Nepotvrđeno stanje", "understand": "Razumijem", + "unlock": "Otključati", "unmatched_currencies": "Valuta vašeg trenutnog novčanika ne odgovara onoj na skeniranom QR-u", "unspent_change": "Promijeniti", "unspent_coins_details_title": "Nepotrošeni detalji o novčićima", @@ -838,6 +843,7 @@ "wallet_menu": "Izbornik", "wallet_name": "Ime novčanika", "wallet_name_exists": "Novčanik s tim nazivom već postoji", + "wallet_password_is_empty": "Lozinka za novčanik je prazna. Lozinka za novčanik ne bi trebala biti prazna", "wallet_recovery_height": "Visina oporavka", "wallet_restoration_store_incorrect_seed_length": "Netočna dužina pristupnog izraza", "wallet_seed": "Pristupni izraz novčanika", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 97a4afd3f..0b8077807 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -236,6 +236,7 @@ "enter_code": "Masukkan kode", "enter_seed_phrase": "Masukkan frasa benih Anda", "enter_totp_code": "Masukkan Kode TOTP.", + "enter_wallet_password": "Masukkan Kata Sandi Dompet", "enter_your_note": "Masukkan catatan Anda...", "enter_your_pin": "Masukkan PIN Anda", "enter_your_pin_again": "Masukkan PIN Anda lagi", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Anda tidak memiliki cukup SOL untuk menutupi biaya transaksi dan menyewa untuk akun tersebut. Mohon tambahkan lebih banyak sol ke dompet Anda atau kurangi jumlah sol yang Anda kirim", "introducing_cake_pay": "Perkenalkan Cake Pay!", "invalid_input": "Masukan tidak valid", + "invalid_password": "Kata sandi salah", "invoice_details": "Detail faktur", "is_percentage": "adalah", "last_30_days": "30 hari terakhir", @@ -501,6 +503,8 @@ "rename": "Ganti nama", "rep_warning": "Peringatan Perwakilan", "rep_warning_sub": "Perwakilan Anda tampaknya tidak bereputasi baik. Ketuk di sini untuk memilih yang baru", + "repeat_wallet_password": "Ulangi Kata Sandi Dompet", + "repeated_password_is_incorrect": "Kata sandi yang diulang tidak benar. Harap ulangi kata sandi dompet lagi.", "require_for_adding_contacts": "Membutuhkan untuk menambahkan kontak", "require_for_all_security_and_backup_settings": "Memerlukan untuk semua pengaturan keamanan dan pencadangan", "require_for_assessing_wallet": "Diperlukan untuk mengakses dompet", @@ -800,6 +804,7 @@ "unavailable_balance_description": "Saldo Tidak Tersedia: Total ini termasuk dana yang terkunci dalam transaksi yang tertunda dan dana yang telah Anda bekukan secara aktif di pengaturan kontrol koin Anda. Saldo yang terkunci akan tersedia setelah transaksi masing-masing selesai, sedangkan saldo yang dibekukan tetap tidak dapat diakses untuk transaksi sampai Anda memutuskan untuk mencairkannya.", "unconfirmed": "Saldo Belum Dikonfirmasi", "understand": "Saya mengerti", + "unlock": "Membuka kunci", "unmatched_currencies": "Mata uang dompet Anda saat ini tidak cocok dengan yang ditandai QR", "unspent_change": "Mengubah", "unspent_coins_details_title": "Rincian koin yang tidak terpakai", @@ -841,6 +846,7 @@ "wallet_menu": "Menu", "wallet_name": "Nama Dompet", "wallet_name_exists": "Nama dompet sudah ada. Silakan pilih nama yang berbeda atau ganti nama dompet yang lain terlebih dahulu.", + "wallet_password_is_empty": "Kata sandi dompet kosong. Kata sandi dompet tidak boleh kosong", "wallet_recovery_height": "Tinggi pemulihan", "wallet_restoration_store_incorrect_seed_length": "Panjang seed yang salah", "wallet_seed": "Seed dompet", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 42c2e628d..b7a81ef9a 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -237,6 +237,7 @@ "enter_code": "Inserisci codice", "enter_seed_phrase": "Inserisci la tua frase di semi", "enter_totp_code": "Inserisci il codice TOTP.", + "enter_wallet_password": "Immettere la password del portafoglio", "enter_your_note": "Inserisci la tua nota…", "enter_your_pin": "Inserisci il tuo PIN", "enter_your_pin_again": "Inserisci il tuo pin di nuovo", @@ -341,6 +342,7 @@ "insufficientFundsForRentError": "Non hai abbastanza SOL per coprire la tassa di transazione e l'affitto per il conto. Si prega di aggiungere più SOL al tuo portafoglio o ridurre l'importo SOL che stai inviando", "introducing_cake_pay": "Presentazione di Cake Pay!", "invalid_input": "Inserimento non valido", + "invalid_password": "Password non valida", "invoice_details": "Dettagli della fattura", "is_percentage": "è", "last_30_days": "Ultimi 30 giorni", @@ -501,6 +503,8 @@ "rename": "Rinomina", "rep_warning": "Avvertenza rappresentativa", "rep_warning_sub": "Il tuo rappresentante non sembra essere in regola. Tocca qui per selezionarne uno nuovo", + "repeat_wallet_password": "Ripeti la password del portafoglio", + "repeated_password_is_incorrect": "La password ripetuta non è corretta. Si prega di ripetere di nuovo la password del portafoglio.", "require_for_adding_contacts": "Richiesto per l'aggiunta di contatti", "require_for_all_security_and_backup_settings": "Richiedi per tutte le impostazioni di sicurezza e backup", "require_for_assessing_wallet": "Richiesto per l'accesso al portafoglio", @@ -799,6 +803,7 @@ "unavailable_balance_description": "Saldo non disponibile: questo totale include i fondi bloccati nelle transazioni in sospeso e quelli che hai congelato attivamente nelle impostazioni di controllo delle monete. I saldi bloccati diventeranno disponibili una volta completate le rispettive transazioni, mentre i saldi congelati rimarranno inaccessibili per le transazioni finché non deciderai di sbloccarli.", "unconfirmed": "Saldo non confermato", "understand": "Capisco", + "unlock": "Sbloccare", "unmatched_currencies": "La valuta del tuo portafoglio attuale non corrisponde a quella del QR scansionato", "unspent_change": "Modifica", "unspent_coins_details_title": "Dettagli sulle monete non spese", @@ -841,6 +846,7 @@ "wallet_menu": "Menù", "wallet_name": "Nome del Portafoglio", "wallet_name_exists": "Il portafoglio con quel nome è già esistito", + "wallet_password_is_empty": "La password del portafoglio è vuota. La password del portafoglio non dovrebbe essere vuota", "wallet_recovery_height": "Altezza di recupero", "wallet_restoration_store_incorrect_seed_length": "Lunghezza seme non corretta", "wallet_seed": "Seme Portafoglio", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 72b1f7d09..a13a4e6f0 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -236,6 +236,7 @@ "enter_code": "コードを入力", "enter_seed_phrase": "シードフレーズを入力してください", "enter_totp_code": "TOTPコードを入力してください。", + "enter_wallet_password": "ウォレットパスワードを入力します", "enter_your_note": "メモを入力してください…", "enter_your_pin": "PINを入力してください", "enter_your_pin_again": "ピンをもう一度入力してください", @@ -341,6 +342,7 @@ "insufficientFundsForRentError": "アカウントの取引料金とレンタルをカバーするのに十分なソルがありません。財布にソルを追加するか、送信するソル量を減らしてください", "introducing_cake_pay": "序章Cake Pay!", "invalid_input": "無効入力", + "invalid_password": "無効なパスワード", "invoice_details": "請求の詳細", "is_percentage": "is", "last_30_days": "過去30日", @@ -500,6 +502,8 @@ "rename": "リネーム", "rep_warning": "代表的な警告", "rep_warning_sub": "あなたの代表者は良好な状態ではないようです。ここをタップして、新しいものを選択します", + "repeat_wallet_password": "ウォレットパスワードを繰り返します", + "repeated_password_is_incorrect": "繰り返しパスワードが正しくありません。ウォレットのパスワードをもう一度繰り返してください。", "require_for_adding_contacts": "連絡先の追加に必要", "require_for_all_security_and_backup_settings": "すべてのセキュリティおよびバックアップ設定に必須", "require_for_assessing_wallet": "ウォレットにアクセスするために必要です", @@ -798,6 +802,7 @@ "unavailable_balance_description": "利用不可能な残高: この合計には、保留中のトランザクションにロックされている資金と、コイン管理設定でアクティブに凍結した資金が含まれます。ロックされた残高は、それぞれの取引が完了すると利用可能になりますが、凍結された残高は、凍結を解除するまで取引にアクセスできません。", "unconfirmed": "残高未確認", "understand": "わかります", + "unlock": "ロックを解除します", "unmatched_currencies": "現在のウォレットの通貨がスキャンされたQRの通貨と一致しません", "unspent_change": "変化", "unspent_coins_details_title": "未使用のコインの詳細", @@ -839,6 +844,7 @@ "wallet_menu": "ウォレットメニュー", "wallet_name": "ウォレット名", "wallet_name_exists": "その名前のウォレットはすでに存在しています", + "wallet_password_is_empty": "ウォレットパスワードは空です。ウォレットのパスワードは空にしてはいけません", "wallet_recovery_height": "回復の高さ", "wallet_restoration_store_incorrect_seed_length": "誤ったシード長s", "wallet_seed": "ウォレットシード", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index b8cfee1b5..d20546f41 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -236,6 +236,7 @@ "enter_code": "코드 입력", "enter_seed_phrase": "시드 문구를 입력하십시오", "enter_totp_code": "TOTP 코드를 입력하세요.", + "enter_wallet_password": "지갑 암호를 입력하십시오", "enter_your_note": "메모를 입력하세요…", "enter_your_pin": "PIN을 입력하십시오", "enter_your_pin_again": "다시 핀을 입력", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "거래 수수료와 계좌 임대료를 충당하기에 충분한 SOL이 없습니다. 지갑에 더 많은 솔을 추가하거나 보내는 솔을 줄이십시오.", "introducing_cake_pay": "소개 Cake Pay!", "invalid_input": "잘못된 입력", + "invalid_password": "유효하지 않은 비밀번호", "invoice_details": "인보이스 세부정보", "is_percentage": "이다", "last_30_days": "지난 30일", @@ -500,6 +502,8 @@ "rename": "이름 바꾸기", "rep_warning": "대표 경고", "rep_warning_sub": "귀하의 대표는 양호한 상태가 아닌 것 같습니다. 새 것을 선택하려면 여기를 탭하십시오", + "repeat_wallet_password": "지갑 암호를 반복하십시오", + "repeated_password_is_incorrect": "반복 된 비밀번호가 올바르지 않습니다. 지갑 암호를 다시 반복하십시오.", "require_for_adding_contacts": "연락처 추가에 필요", "require_for_all_security_and_backup_settings": "모든 보안 및 백업 설정에 필요", "require_for_assessing_wallet": "지갑 접근을 위해 필요", @@ -798,6 +802,7 @@ "unavailable_balance_description": "사용할 수 없는 잔액: 이 총계에는 보류 중인 거래에 잠겨 있는 자금과 코인 관리 설정에서 적극적으로 동결된 자금이 포함됩니다. 잠긴 잔액은 해당 거래가 완료되면 사용할 수 있게 되며, 동결된 잔액은 동결을 해제하기 전까지 거래에 액세스할 수 없습니다.", "unconfirmed": "확인되지 않은 잔액", "understand": "이해 했어요", + "unlock": "터놓다", "unmatched_currencies": "현재 지갑의 통화가 스캔한 QR의 통화와 일치하지 않습니다.", "unspent_change": "변화", "unspent_coins_details_title": "사용하지 않은 동전 세부 정보", @@ -839,6 +844,7 @@ "wallet_menu": "월렛 메뉴", "wallet_name": "지갑 이름", "wallet_name_exists": "해당 이름의 지갑이 이미 존재합니다.", + "wallet_password_is_empty": "지갑 암호는 비어 있습니다. 지갑 암호는 비어 있지 않아야합니다", "wallet_recovery_height": "복구 높이", "wallet_restoration_store_incorrect_seed_length": "시드 길이가 잘못되었습니다", "wallet_seed": "지갑 시드", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 52fe72ea6..06d7cf627 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -236,6 +236,7 @@ "enter_code": "ကုဒ်ထည့်ပါ။", "enter_seed_phrase": "သင့်ရဲ့မျိုးစေ့စကားစုကိုရိုက်ထည့်ပါ", "enter_totp_code": "ကျေးဇူးပြု၍ TOTP ကုဒ်ကို ထည့်ပါ။", + "enter_wallet_password": "ပိုက်ဆံအိတ်စကားဝှက်ကိုရိုက်ထည့်ပါ", "enter_your_note": "သင့်မှတ်စုကို ထည့်ပါ...", "enter_your_pin": "သင်၏ PIN ကိုထည့်ပါ။", "enter_your_pin_again": "သင့်ပင်နံပါတ်ကို ထပ်မံထည့်သွင်းပါ။", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "သင်ငွေပေးချေမှုအခကြေးငွေကိုဖုံးအုပ်ရန်နှင့်အကောင့်ငှားရန်လုံလောက်သော sol ရှိသည်မဟုတ်ကြဘူး။ ကြင်နာစွာသင်၏ပိုက်ဆံအိတ်သို့ပိုမို sol ကိုပိုမိုထည့်ပါသို့မဟုတ်သင်ပို့ခြင်း sol ပမာဏကိုလျှော့ချပါ", "introducing_cake_pay": "Cake Pay ကို မိတ်ဆက်ခြင်း။", "invalid_input": "ထည့်သွင်းမှု မမှန်ကန်ပါ။", + "invalid_password": "မမှန်ကန်သောစကားဝှက်", "invoice_details": "ပြေစာအသေးစိတ်", "is_percentage": "သည်", "last_30_days": "လွန်ခဲ့သော ရက် 30", @@ -499,6 +501,8 @@ "rename": "အမည်ပြောင်းပါ။", "rep_warning": "ကိုယ်စားလှယ်သတိပေးချက်", "rep_warning_sub": "သင်၏ကိုယ်စားလှယ်သည်ကောင်းမွန်သောရပ်တည်မှုတွင်မဖြစ်သင့်ပါ။ အသစ်တစ်ခုကိုရွေးချယ်ရန်ဤနေရာတွင်အသာပုတ်ပါ", + "repeat_wallet_password": "ပိုက်ဆံအိတ်စကားဝှက်ကိုပြန်လုပ်ပါ", + "repeated_password_is_incorrect": "ထပ်ခါတလဲလဲစကားဝှက်မမှန်ကန်ပါ ကျေးဇူးပြုပြီးပိုက်ဆံအိတ်စကားဝှက်ကိုပြန်လုပ်ပါ။", "require_for_adding_contacts": "အဆက်အသွယ်များထည့်ရန် လိုအပ်သည်။", "require_for_all_security_and_backup_settings": "လုံခြုံရေးနှင့် အရန်ဆက်တင်များအားလုံးအတွက် လိုအပ်ပါသည်။", "require_for_assessing_wallet": "ပိုက်ဆံအိတ်ကို ဝင်သုံးရန် လိုအပ်သည်။", @@ -797,6 +801,7 @@ "unavailable_balance_description": "မရရှိနိုင်သော လက်ကျန်ငွေ- ဤစုစုပေါင်းတွင် ဆိုင်းငံ့ထားသော ငွေပေးငွေယူများတွင် သော့ခတ်ထားသော ငွေကြေးများနှင့် သင်၏ coin ထိန်းချုပ်မှုဆက်တင်များတွင် သင် တက်ကြွစွာ အေးခဲထားသော ငွေများ ပါဝင်သည်။ သော့ခတ်ထားသော လက်ကျန်ငွေများကို ၎င်းတို့၏ သက်ဆိုင်ရာ ငွေပေးငွေယူများ ပြီးမြောက်သည်နှင့် တပြိုင်နက် ရရှိနိုင်မည်ဖြစ်ပြီး၊ အေးခဲထားသော လက်ကျန်များကို ၎င်းတို့အား ပြန်ဖြုတ်ရန် သင်ဆုံးဖြတ်သည်အထိ ငွေပေးငွေယူများအတွက် ဆက်လက်၍မရနိုင်ပါ။", "unconfirmed": "အတည်မပြုနိုင်သော လက်ကျန်ငွေ", "understand": "ကျွန်တော်နားလည်ပါတယ်", + "unlock": "သော့ဖွင့်", "unmatched_currencies": "သင့်လက်ရှိပိုက်ဆံအိတ်၏ငွေကြေးသည် စကင်ဖတ်ထားသော QR နှင့် မကိုက်ညီပါ။", "unspent_change": "ပေြာင်းလဲခြင်း", "unspent_coins_details_title": "အသုံးမဝင်သော အကြွေစေ့အသေးစိတ်များ", @@ -838,6 +843,7 @@ "wallet_menu": "မီနူး", "wallet_name": "ပိုက်ဆံအိတ်နာမည", "wallet_name_exists": "ထိုအမည်ဖြင့် ပိုက်ဆံအိတ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ သို့မဟုတ် အခြားပိုက်ဆံအိတ်ကို ဦးစွာ အမည်ပြောင်းပါ။", + "wallet_password_is_empty": "ပိုက်ဆံအိတ်စကားဝှက်သည်ဗလာဖြစ်သည်။ ပိုက်ဆံအိတ်စကားဝှက်သည်အချည်းနှီးဖြစ်သင့်သည်", "wallet_recovery_height": "ပြန်လည်ထူထောင်ရေးအမြင့်", "wallet_restoration_store_incorrect_seed_length": "မျိုးစေ့အရှည် မမှန်ပါ။", "wallet_seed": "ပိုက်ဆံအိတ်စေ့", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index cde10506f..78caef912 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -236,6 +236,7 @@ "enter_code": "Voer code in", "enter_seed_phrase": "Voer uw zaadzin in", "enter_totp_code": "Voer de TOTP-code in.", + "enter_wallet_password": "Voer het Wallet -wachtwoord in", "enter_your_note": "Voer uw notitie in ...", "enter_your_pin": "Voer uw pincode in", "enter_your_pin_again": "Voer uw PIN opnieuw in", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "U hebt niet genoeg SOL om de transactiekosten en huur voor de rekening te dekken. Voeg vriendelijk meer SOL toe aan uw portemonnee of verminder de SOL -hoeveelheid die u verzendt", "introducing_cake_pay": "Introductie van Cake Pay!", "invalid_input": "Ongeldige invoer", + "invalid_password": "Ongeldig wachtwoord", "invoice_details": "Factuurgegevens", "is_percentage": "is", "last_30_days": "Laatste 30 dagen", @@ -499,6 +501,8 @@ "rename": "Hernoemen", "rep_warning": "Representatieve waarschuwing", "rep_warning_sub": "Uw vertegenwoordiger lijkt niet goed te staan. Tik hier om een ​​nieuwe te selecteren", + "repeat_wallet_password": "Herhaal het Wallet -wachtwoord", + "repeated_password_is_incorrect": "Herhaald wachtwoord is onjuist. Herhaal het Wallet -wachtwoord opnieuw.", "require_for_adding_contacts": "Vereist voor het toevoegen van contacten", "require_for_all_security_and_backup_settings": "Vereist voor alle beveiligings- en back-upinstellingen", "require_for_assessing_wallet": "Vereist voor toegang tot portemonnee", @@ -797,6 +801,7 @@ "unavailable_balance_description": "Niet-beschikbaar saldo: Dit totaal omvat het geld dat is vergrendeld in lopende transacties en het geld dat u actief hebt bevroren in uw muntcontrole-instellingen. Vergrendelde saldi komen beschikbaar zodra de betreffende transacties zijn voltooid, terwijl bevroren saldi ontoegankelijk blijven voor transacties totdat u besluit ze weer vrij te geven.", "unconfirmed": "Onbevestigd saldo", "understand": "Ik begrijp het", + "unlock": "Ontgrendelen", "unmatched_currencies": "De valuta van uw huidige portemonnee komt niet overeen met die van de gescande QR", "unspent_change": "Wijziging", "unspent_coins_details_title": "Details van niet-uitgegeven munten", @@ -839,6 +844,7 @@ "wallet_menu": "Portemonnee-menu", "wallet_name": "Portemonnee naam", "wallet_name_exists": "Portemonnee met die naam bestaat al", + "wallet_password_is_empty": "Wallet -wachtwoord is leeg. Wallet -wachtwoord mag niet leeg zijn", "wallet_recovery_height": "Herstelhoogte", "wallet_restoration_store_incorrect_seed_length": "Onjuiste zaadlengte", "wallet_seed": "Portemonnee zaad", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index a22034c96..7de435319 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -236,6 +236,7 @@ "enter_code": "Wprowadź kod", "enter_seed_phrase": "Wprowadź swoją frazę nasienną", "enter_totp_code": "Wprowadź kod TOTP.", + "enter_wallet_password": "Wprowadź hasło portfela", "enter_your_note": "Wpisz notatkę…", "enter_your_pin": "Wpisz kod PIN", "enter_your_pin_again": "Wprowadź ponownie swój kod PIN", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Nie masz wystarczającej ilości SOL, aby pokryć opłatę za transakcję i czynsz za konto. Uprzejmie dodaj więcej sol do portfela lub zmniejsz solę, którą wysyłasz", "introducing_cake_pay": "Przedstawiamy Cake Pay!", "invalid_input": "Nieprawidłowe dane wejściowe", + "invalid_password": "Nieprawidłowe hasło", "invoice_details": "Dane do faktury", "is_percentage": "jest", "last_30_days": "Ostatnie 30 dni", @@ -499,6 +501,8 @@ "rename": "Zmień nazwę", "rep_warning": "Przedstawicielskie ostrzeżenie", "rep_warning_sub": "Twój przedstawiciel nie wydaje się mieć dobrej opinii. Stuknij tutaj, aby wybrać nowy", + "repeat_wallet_password": "Powtórz hasło portfela", + "repeated_password_is_incorrect": "Powtarzane hasło jest nieprawidłowe. Powtórz ponownie hasło portfela.", "require_for_adding_contacts": "Wymagane do dodania kontaktów", "require_for_all_security_and_backup_settings": "Wymagaj dla wszystkich ustawień zabezpieczeń i kopii zapasowych", "require_for_assessing_wallet": "Wymagaj dostępu do portfela", @@ -797,6 +801,7 @@ "unavailable_balance_description": "Niedostępne saldo: Suma ta obejmuje środki zablokowane w transakcjach oczekujących oraz te, które aktywnie zamroziłeś w ustawieniach kontroli monet. Zablokowane salda staną się dostępne po zakończeniu odpowiednich transakcji, natomiast zamrożone salda pozostaną niedostępne dla transakcji, dopóki nie zdecydujesz się ich odblokować.", "unconfirmed": "Niepotwierdzone saldo", "understand": "Rozumiem", + "unlock": "Odblokować", "unmatched_currencies": "Waluta Twojego obecnego portfela nie zgadza się z waluctą zeskanowanego kodu QR", "unspent_change": "Zmiana", "unspent_coins_details_title": "Szczegóły niewydanych monet", @@ -838,6 +843,7 @@ "wallet_menu": "Menu portfela", "wallet_name": "Nazwa portfela", "wallet_name_exists": "Portfel o tej nazwie już istnieje", + "wallet_password_is_empty": "Hasło portfela jest puste. Hasło portfela nie powinno być puste", "wallet_recovery_height": "Wysokość odzysku", "wallet_restoration_store_incorrect_seed_length": "Nieprawidłowa długość frazy seed", "wallet_seed": "Seed portfela", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 8f87ca59f..a3d789cab 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -236,6 +236,7 @@ "enter_code": "Digite o código", "enter_seed_phrase": "Digite sua frase de semente", "enter_totp_code": "Digite o código TOTP.", + "enter_wallet_password": "Digite a senha da carteira", "enter_your_note": "Insira sua nota ...", "enter_your_pin": "Insira seu PIN", "enter_your_pin_again": "Insira seu PIN novamente", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Você não tem Sol suficiente para cobrir a taxa de transação e o aluguel da conta. Por favor, adicione mais sol à sua carteira ou reduza a quantidade de sol que você envia", "introducing_cake_pay": "Apresentando o Cake Pay!", "invalid_input": "Entrada inválida", + "invalid_password": "Senha inválida", "invoice_details": "Detalhes da fatura", "is_percentage": "é", "last_30_days": "Últimos 30 dias", @@ -501,6 +503,8 @@ "rename": "Renomear", "rep_warning": "Aviso representativo", "rep_warning_sub": "Seu representante não parece estar em boa posição. Toque aqui para selecionar um novo", + "repeat_wallet_password": "Repita a senha da carteira", + "repeated_password_is_incorrect": "A senha repetida está incorreta. Repita a senha da carteira novamente.", "require_for_adding_contacts": "Requer para adicionar contatos", "require_for_all_security_and_backup_settings": "Exigir todas as configurações de segurança e backup", "require_for_assessing_wallet": "Requer para acessar a carteira", @@ -799,6 +803,7 @@ "unavailable_balance_description": "Saldo Indisponível: Este total inclui fundos bloqueados em transações pendentes e aqueles que você congelou ativamente nas configurações de controle de moedas. Os saldos bloqueados ficarão disponíveis assim que suas respectivas transações forem concluídas, enquanto os saldos congelados permanecerão inacessíveis para transações até que você decida descongelá-los.", "unconfirmed": "Saldo não confirmado", "understand": "Entendo", + "unlock": "Desbloquear", "unmatched_currencies": "A moeda da sua carteira atual não corresponde à do QR digitalizado", "unspent_change": "Troco", "unspent_coins_details_title": "Detalhes de moedas não gastas", @@ -841,6 +846,7 @@ "wallet_menu": "Menu", "wallet_name": "Nome da carteira", "wallet_name_exists": "A carteira com esse nome já existe", + "wallet_password_is_empty": "A senha da carteira está vazia. A senha da carteira não deve estar vazia", "wallet_recovery_height": "Altura de recuperação", "wallet_restoration_store_incorrect_seed_length": "Comprimento de semente incorreto", "wallet_seed": "Semente de carteira", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 9f360137f..d84aa146f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -236,6 +236,7 @@ "enter_code": "Введите код", "enter_seed_phrase": "Введите свою семенную фразу", "enter_totp_code": "Пожалуйста, введите TOTP-код.", + "enter_wallet_password": "Введите пароль кошелька", "enter_your_note": "Введите примечание…", "enter_your_pin": "Введите ваш PIN", "enter_your_pin_again": "Введите PIN еще раз", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "У вас недостаточно Sol, чтобы покрыть плату за транзакцию и аренду для счета. Пожалуйста, добавьте больше Sol в свой кошелек или уменьшите сумму Sol, которую вы отправляете", "introducing_cake_pay": "Представляем Cake Pay!", "invalid_input": "Неверный Ввод", + "invalid_password": "Неверный пароль", "invoice_details": "Детали счета", "is_percentage": "есть", "last_30_days": "Последние 30 дней", @@ -500,6 +502,8 @@ "rename": "Переименовать", "rep_warning": "Представительное предупреждение", "rep_warning_sub": "Ваш представитель, похоже, не в хорошей репутации. Нажмите здесь, чтобы выбрать новый", + "repeat_wallet_password": "Повторите пароль кошелька", + "repeated_password_is_incorrect": "Повторный пароль неверен. Пожалуйста, повторите пароль кошелька снова.", "require_for_adding_contacts": "Требовать добавления контактов", "require_for_all_security_and_backup_settings": "Требовать все настройки безопасности и резервного копирования", "require_for_assessing_wallet": "Требовать для доступа к кошельку", @@ -798,6 +802,7 @@ "unavailable_balance_description": "Недоступный баланс: в эту сумму входят средства, заблокированные в ожидающих транзакциях, и средства, которые вы активно заморозили в настройках управления монетами. Заблокированные балансы станут доступны после завершения соответствующих транзакций, а замороженные балансы останутся недоступными для транзакций, пока вы не решите их разморозить.", "unconfirmed": "Неподтвержденный баланс", "understand": "Понятно", + "unlock": "Разблокировать", "unmatched_currencies": "Валюта вашего текущего кошелька не соответствует валюте отсканированного QR-кода.", "unspent_change": "Изменять", "unspent_coins_details_title": "Сведения о неизрасходованных монетах", @@ -839,6 +844,7 @@ "wallet_menu": "Меню кошелька", "wallet_name": "Имя кошелька", "wallet_name_exists": "Кошелек с таким именем уже существует", + "wallet_password_is_empty": "Пароль кошелька пуст. Пароль кошелька не должен быть пустым", "wallet_recovery_height": "Высота восстановления", "wallet_restoration_store_incorrect_seed_length": "Неверная длина мнемонической фразы", "wallet_seed": "Мнемоническая фраза кошелька", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index a178d2452..2adccb2cf 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -236,6 +236,7 @@ "enter_code": "กรอกรหัส", "enter_seed_phrase": "ป้อนวลีเมล็ดพันธุ์ของคุณ", "enter_totp_code": "กรุณาใส่รหัสทีโอที", + "enter_wallet_password": "ป้อนรหัสผ่านกระเป๋าเงิน", "enter_your_note": "ใส่บันทึกของคุณ...", "enter_your_pin": "ใส่ PIN ของคุณ", "enter_your_pin_again": "ใส่ PIN ของคุณอีกครั้ง", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "คุณไม่มีโซลเพียงพอที่จะครอบคลุมค่าธรรมเนียมการทำธุรกรรมและค่าเช่าสำหรับบัญชี กรุณาเพิ่มโซลให้มากขึ้นลงในกระเป๋าเงินของคุณหรือลดจำนวนโซลที่คุณส่งมา", "introducing_cake_pay": "ยินดีต้อนรับสู่ Cake Pay!", "invalid_input": "อินพุตไม่ถูกต้อง", + "invalid_password": "รหัสผ่านไม่ถูกต้อง", "invoice_details": "รายละเอียดใบแจ้งหนี้", "is_percentage": "เป็น", "last_30_days": "30 วันล่าสุด", @@ -499,6 +501,8 @@ "rename": "เปลี่ยนชื่อ", "rep_warning": "คำเตือนตัวแทน", "rep_warning_sub": "ตัวแทนของคุณดูเหมือนจะไม่อยู่ในสถานะที่ดี แตะที่นี่เพื่อเลือกอันใหม่", + "repeat_wallet_password": "ทำซ้ำรหัสผ่านกระเป๋าเงิน", + "repeated_password_is_incorrect": "รหัสผ่านซ้ำไม่ถูกต้อง โปรดทำซ้ำรหัสผ่านกระเป๋าเงินอีกครั้ง", "require_for_adding_contacts": "ต้องการสำหรับการเพิ่มผู้ติดต่อ", "require_for_all_security_and_backup_settings": "จำเป็นสำหรับการตั้งค่าความปลอดภัยและการสำรองข้อมูลทั้งหมด", "require_for_assessing_wallet": "จำเป็นสำหรับการเข้าถึงกระเป๋าเงิน", @@ -797,6 +801,7 @@ "unavailable_balance_description": "ยอดคงเหลือที่ไม่พร้อมใช้งาน: ยอดรวมนี้รวมถึงเงินทุนที่ถูกล็อคในการทำธุรกรรมที่รอดำเนินการและที่คุณได้แช่แข็งไว้ในการตั้งค่าการควบคุมเหรียญของคุณ ยอดคงเหลือที่ถูกล็อคจะพร้อมใช้งานเมื่อธุรกรรมที่เกี่ยวข้องเสร็จสมบูรณ์ ในขณะที่ยอดคงเหลือที่แช่แข็งจะไม่สามารถเข้าถึงได้สำหรับธุรกรรมจนกว่าคุณจะตัดสินใจยกเลิกการแช่แข็ง", "unconfirmed": "ยอดคงเหลือที่ไม่ได้รับการยืนยัน", "understand": "ฉันเข้าใจ", + "unlock": "ปลดล็อค", "unmatched_currencies": "สกุลเงินของกระเป๋าปัจจุบันของคุณไม่ตรงกับของ QR ที่สแกน", "unspent_change": "เปลี่ยน", "unspent_coins_details_title": "รายละเอียดเหรียญที่ไม่ได้ใช้", @@ -838,6 +843,7 @@ "wallet_menu": "เมนู", "wallet_name": "ชื่อกระเป๋า", "wallet_name_exists": "กระเป๋าที่มีชื่อนี้มีอยู่แล้ว โปรดเลือกชื่ออื่นหรือเปลี่ยนชื่อกระเป๋าอื่นก่อน", + "wallet_password_is_empty": "รหัสผ่านกระเป๋าเงินว่างเปล่า รหัสผ่านกระเป๋าเงินไม่ควรว่างเปล่า", "wallet_recovery_height": "ความสูงของการกู้คืน", "wallet_restoration_store_incorrect_seed_length": "ความยาวของซีดไม่ถูกต้อง", "wallet_seed": "ซีดของกระเป๋า", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index f49d3ddee..3bbae2e50 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -236,6 +236,7 @@ "enter_code": "Ipasok ang code", "enter_seed_phrase": "Ipasok ang iyong pariralang binhi", "enter_totp_code": "Mangyaring ipasok ang TOTP code.", + "enter_wallet_password": "Ipasok ang password ng pitaka", "enter_your_note": "Ipasok ang iyong tala ...", "enter_your_pin": "Ipasok ang iyong pin", "enter_your_pin_again": "Ipasok muli ang iyong pin", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "Wala kang sapat na sol upang masakop ang bayad sa transaksyon at upa para sa account. Mabait na magdagdag ng higit pa sa iyong pitaka o bawasan ang halaga ng sol na iyong ipinapadala", "introducing_cake_pay": "Ipinakikilala ang cake pay!", "invalid_input": "Di -wastong input", + "invalid_password": "Di wastong password", "invoice_details": "Mga detalye ng invoice", "is_percentage": "ay", "last_30_days": "Huling 30 araw", @@ -499,6 +501,8 @@ "rename": "Palitan ang pangalan", "rep_warning": "Babala ng kinatawan", "rep_warning_sub": "Ang iyong kinatawan ay hindi lilitaw na nasa mabuting kalagayan. Tapikin dito upang pumili ng bago", + "repeat_wallet_password": "Ulitin ang password ng pitaka", + "repeated_password_is_incorrect": "Ang paulit -ulit na password ay hindi tama. Mangyaring ulitin muli ang password ng pitaka.", "require_for_adding_contacts": "Nangangailangan para sa pagdaragdag ng mga contact", "require_for_all_security_and_backup_settings": "Nangangailangan para sa lahat ng mga setting ng seguridad at backup", "require_for_assessing_wallet": "Nangangailangan para sa pag -access ng pitaka", @@ -797,6 +801,7 @@ "unavailable_balance_description": "Hindi Available na Balanse: Kasama sa kabuuang ito ang mga pondong naka-lock sa mga nakabinbing transaksyon at ang mga aktibong na-freeze mo sa iyong mga setting ng kontrol ng coin. Magiging available ang mga naka-lock na balanse kapag nakumpleto na ang kani-kanilang mga transaksyon, habang ang mga nakapirming balanse ay nananatiling hindi naa-access para sa mga transaksyon hanggang sa magpasya kang i-unfreeze ang mga ito.", "unconfirmed": "Hindi nakumpirma na balanse", "understand": "naiintindihan ko", + "unlock": "I -unlock", "unmatched_currencies": "Ang pera ng iyong kasalukuyang pitaka ay hindi tumutugma sa na -scan na QR", "unspent_change": "Baguhin", "unspent_coins_details_title": "Mga Detalye ng Unspent Coins", @@ -838,6 +843,7 @@ "wallet_menu": "Menu", "wallet_name": "Pangalan ng Wallet", "wallet_name_exists": "Ang isang pitaka na may pangalang iyon ay mayroon na. Mangyaring pumili ng ibang pangalan o palitan muna ang iba pang pitaka.", + "wallet_password_is_empty": "Walang laman ang password ng wallet. Ang password ng wallet ay hindi dapat walang laman", "wallet_recovery_height": "Taas ng pagbawi", "wallet_restoration_store_incorrect_seed_length": "Maling haba ng binhi", "wallet_seed": "SEED ng Wallet", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index c73765f64..b47787bbd 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -236,6 +236,7 @@ "enter_code": "Kodu girin", "enter_seed_phrase": "Tohum ifadenizi girin", "enter_totp_code": "Lütfen TOTP Kodunu giriniz.", + "enter_wallet_password": "Cüzdan şifresini girin", "enter_your_note": "Notunu gir…", "enter_your_pin": "PIN'ini gir", "enter_your_pin_again": "PIN kodunu tekrar girin", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "İşlem ücretini karşılamak ve hesap için kiralamak için yeterli SOL'nuz yok. Lütfen cüzdanınıza daha fazla sol ekleyin veya gönderdiğiniz sol miktarını azaltın", "introducing_cake_pay": "Cake Pay ile tanışın!", "invalid_input": "Geçersiz Giriş", + "invalid_password": "Geçersiz şifre", "invoice_details": "fatura detayları", "is_percentage": "is", "last_30_days": "Son 30 gün", @@ -499,6 +501,8 @@ "rename": "Yeniden adlandır", "rep_warning": "Temsilci uyarı", "rep_warning_sub": "Temsilciniz iyi durumda görünmüyor. Yeni bir tane seçmek için buraya dokunun", + "repeat_wallet_password": "Cüzdan şifresini tekrarlayın", + "repeated_password_is_incorrect": "Tekrarlanan şifre yanlış. Lütfen cüzdan şifresini tekrarlayın.", "require_for_adding_contacts": "Kişi eklemek için gerekli", "require_for_all_security_and_backup_settings": "Tüm güvenlik ve yedekleme ayarları için iste", "require_for_assessing_wallet": "Cüzdana erişmek için gerekli", @@ -797,6 +801,7 @@ "unavailable_balance_description": "Kullanılamayan Bakiye: Bu toplam, bekleyen işlemlerde kilitlenen fonları ve jeton kontrol ayarlarınızda aktif olarak dondurduğunuz fonları içerir. Kilitli bakiyeler, ilgili işlemleri tamamlandıktan sonra kullanılabilir hale gelir; dondurulmuş bakiyeler ise siz onları dondurmaya karar verene kadar işlemler için erişilemez durumda kalır.", "unconfirmed": "Onaylanmamış Bakiye", "understand": "Anladım", + "unlock": "Kilidini aç", "unmatched_currencies": "Mevcut cüzdanınızın para birimi taranan QR ile eşleşmiyor", "unspent_change": "Değiştirmek", "unspent_coins_details_title": "Harcanmamış koin detayları", @@ -838,6 +843,7 @@ "wallet_menu": "Menü", "wallet_name": "Cüzdan ismi", "wallet_name_exists": "Bu isimde bir cüzdan zaten mevcut. Lütfen farklı bir isim seç veya önce diğer cüzdanı yeniden adlandır.", + "wallet_password_is_empty": "Cüzdan şifresi boş. Cüzdan şifresi boş olmamalı", "wallet_recovery_height": "Kurtarma Yüksekliği", "wallet_restoration_store_incorrect_seed_length": "Yanlış tohum uzunluğu", "wallet_seed": "Cüzdan tohumu", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index d088dd1b2..328548087 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -236,6 +236,7 @@ "enter_code": "Введіть код", "enter_seed_phrase": "Введіть свою насіннєву фразу", "enter_totp_code": "Будь ласка, введіть код TOTP.", + "enter_wallet_password": "Введіть пароль гаманця", "enter_your_note": "Введіть примітку…", "enter_your_pin": "Введіть ваш PIN", "enter_your_pin_again": "Введіть PIN ще раз", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "У вас недостатньо SOL, щоб покрити плату за транзакцію та оренду на рахунок. Будь ласка, додайте до свого гаманця більше SOL або зменшіть суму, яку ви надсилаєте", "introducing_cake_pay": "Представляємо Cake Pay!", "invalid_input": "Неправильні дані", + "invalid_password": "Недійсний пароль", "invoice_details": "Реквізити рахунку-фактури", "is_percentage": "є", "last_30_days": "Останні 30 днів", @@ -500,6 +502,8 @@ "rename": "Перейменувати", "rep_warning": "Представницьке попередження", "rep_warning_sub": "Ваш представник, схоже, не має доброго становища. Торкніться тут, щоб вибрати новий", + "repeat_wallet_password": "Повторіть пароль гаманця", + "repeated_password_is_incorrect": "Повторний пароль невірний. Будь ласка, повторіть пароль гаманця ще раз.", "require_for_adding_contacts": "Потрібен для додавання контактів", "require_for_all_security_and_backup_settings": "Вимагати всіх налаштувань безпеки та резервного копіювання", "require_for_assessing_wallet": "Потрібен доступ до гаманця", @@ -798,6 +802,7 @@ "unavailable_balance_description": "Недоступний баланс: ця сума включає кошти, заблоковані в незавершених транзакціях, і ті, які ви активно заморозили в налаштуваннях контролю монет. Заблоковані баланси стануть доступними після завершення відповідних транзакцій, тоді як заморожені баланси залишаються недоступними для транзакцій, доки ви не вирішите їх розморозити.", "unconfirmed": "Непідтверджений баланс", "understand": "Зрозуміло", + "unlock": "Розблокувати", "unmatched_currencies": "Валюта вашого гаманця не збігається з валютою сканованого QR-коду", "unspent_change": "Зміна", "unspent_coins_details_title": "Відомості про невитрачені монети", @@ -839,6 +844,7 @@ "wallet_menu": "Меню гаманця", "wallet_name": "Ім'я гаманця", "wallet_name_exists": "Гаманець з такою назвою вже існує", + "wallet_password_is_empty": "Пароль гаманця порожній. Пароль гаманця не повинен бути порожнім", "wallet_recovery_height": "Висота відновлення", "wallet_restoration_store_incorrect_seed_length": "Невірна довжина мнемонічної фрази", "wallet_seed": "Мнемонічна фраза гаманця", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 0694463de..7d794f9bb 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -236,6 +236,7 @@ "enter_code": "کوڈ درج کریں", "enter_seed_phrase": "اپنے بیج کا جملہ درج کریں", "enter_totp_code": "براہ کرم TOTP کوڈ درج کریں۔", + "enter_wallet_password": "پرس کا پاس ورڈ درج کریں", "enter_your_note": "اپنا نوٹ درج کریں…", "enter_your_pin": "اپنا PIN درج کریں۔", "enter_your_pin_again": "اپنا پن دوبارہ درج کریں۔", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "آپ کے پاس ٹرانزیکشن فیس اور اکاؤنٹ کے لئے کرایہ لینے کے ل enough اتنا SOL نہیں ہے۔ برائے مہربانی اپنے بٹوے میں مزید سول شامل کریں یا آپ کو بھیجنے والی سول رقم کو کم کریں", "introducing_cake_pay": "Cake پے کا تعارف!", "invalid_input": "غلط ان پٹ", + "invalid_password": "غلط پاسورڈ", "invoice_details": "رسید کی تفصیلات", "is_percentage": "ہے", "last_30_days": "آخری 30 دن", @@ -501,6 +503,8 @@ "rename": "نام تبدیل کریں۔", "rep_warning": "نمائندہ انتباہ", "rep_warning_sub": "آپ کا نمائندہ اچھ standing ے مقام پر نہیں دکھائی دیتا ہے۔ نیا منتخب کرنے کے لئے یہاں ٹیپ کریں", + "repeat_wallet_password": "بٹوے کا پاس ورڈ دہرائیں", + "repeated_password_is_incorrect": "بار بار پاس ورڈ غلط ہے۔ براہ کرم دوبارہ پرس کا پاس ورڈ دہرائیں۔", "require_for_adding_contacts": "رابطوں کو شامل کرنے کی ضرورت ہے۔", "require_for_all_security_and_backup_settings": "تمام سیکورٹی اور بیک اپ کی ترتیبات کے لیے درکار ہے۔", "require_for_assessing_wallet": "بٹوے تک رسائی کے لیے درکار ہے۔", @@ -799,6 +803,7 @@ "unavailable_balance_description": "۔ﮯﺗﺮﮐ ﮟﯿﮩﻧ ﮧﻠﺼﯿﻓ ﺎﮐ ﮯﻧﺮﮐ ﺪﻤﺠﻨﻣ ﻥﺍ ﮟﯿﮩﻧﺍ ﭖﺁ ﮧﮐ ﮏﺗ ﺐﺟ ﮟﯿﮨ ﮯﺘﮨﺭ ﯽﺋﺎﺳﺭ ﻞﺑﺎﻗﺎﻧ ﮏﺗ ﺖﻗﻭ ﺱﺍ ﮯﯿﻟ ﮯﮐ ﻦﯾﺩ ﻦﯿﻟ ﺲﻨﻠﯿﺑ ﺪﻤﺠﻨﻣ ﮧﮐ ﺐﺟ ،ﮯﮔ ﮟﯿﺋﺎﺟ ﻮﮨ ﺏﺎﯿﺘﺳﺩ ﺲﻨﻠﯿﺑ ﻞﻔﻘﻣ ﺪﻌﺑ ﮯﮐ ﮯﻧﻮﮨ ﻞﻤﮑﻣ ﻦﯾﺩ ﻦﯿﻟ ﮧﻘﻠﻌﺘﻣ ﮯﮐ ﻥﺍ ۔ﮯﮨ ﺎﮭﮐﺭ ﺮ", "unconfirmed": "غیر تصدیق شدہ بیلنس", "understand": "میں سمجھتا ہوں۔", + "unlock": "غیر مقفل", "unmatched_currencies": "آپ کے پرس کی موجودہ کرنسی اسکین شدہ QR سے مماثل نہیں ہے۔", "unspent_change": "تبدیل کریں", "unspent_coins_details_title": "غیر خرچ شدہ سککوں کی تفصیلات", @@ -840,6 +845,7 @@ "wallet_menu": "مینو", "wallet_name": "بٹوے کا نام", "wallet_name_exists": "اس نام کا پرس پہلے سے موجود ہے۔ براہ کرم ایک مختلف نام منتخب کریں یا پہلے دوسرے بٹوے کا نام تبدیل کریں۔", + "wallet_password_is_empty": "پرس کا پاس ورڈ خالی ہے۔ پرس کا پاس ورڈ خالی نہیں ہونا چاہئے", "wallet_recovery_height": "بحالی کی اونچائی", "wallet_restoration_store_incorrect_seed_length": "غلط بیج کی لمبائی", "wallet_seed": "بٹوے کا بیج", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 87df87aca..2150a503f 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -237,6 +237,7 @@ "enter_code": "Tẹ̀ ọ̀rọ̀", "enter_seed_phrase": "Tẹ ọrọ-iru irugbin rẹ", "enter_totp_code": "Jọwọ pọ koodu TOTP.", + "enter_wallet_password": "Tẹ ọrọ igbaniwọle apamọwọ", "enter_your_note": "Tẹ̀ àkọsílẹ̀ yín", "enter_your_pin": "Tẹ̀ òǹkà ìdánimọ̀ àdáni yín", "enter_your_pin_again": "Tún òǹkà ìdánimọ̀ àdáni yín tẹ̀", @@ -341,6 +342,7 @@ "insufficientFundsForRentError": "O ko ni Sol kan lati bo owo isanwo naa ki o yalo fun iroyin naa. Fi agbara kun Sol diẹ sii si apamọwọ rẹ tabi dinku soso naa ti o \\ 'tun n firanṣẹ", "introducing_cake_pay": "Ẹ bá Cake Pay!", "invalid_input": "Iṣawọle ti ko tọ", + "invalid_password": "Ọrọ igbaniwọle ti ko wulo", "invoice_details": "Iru awọn ẹya ọrọ", "is_percentage": "jẹ́", "last_30_days": "Ọ̀jọ̀ mọ́gbọ̀n tó kọjà", @@ -500,6 +502,8 @@ "rename": "Pààrọ̀ orúkọ", "rep_warning": "Ikilọ aṣoju", "rep_warning_sub": "Aṣoju rẹ ko han lati wa ni iduro to dara. Fọwọ ba ibi lati yan ọkan titun kan", + "repeat_wallet_password": "Tun ọrọ igbaniwọle apamọwọ naa", + "repeated_password_is_incorrect": "Ọrọ igbaniwọle tun jẹ aṣiṣe. Jọwọ tun ọrọigbaniwọle apamọwọ lẹẹkansi.", "require_for_adding_contacts": "Beere fun fifi awọn olubasọrọ kun", "require_for_all_security_and_backup_settings": "Beere fun gbogbo aabo ati awọn eto afẹyinti", "require_for_assessing_wallet": "Beere fun wiwọle si apamọwọ", @@ -798,6 +802,7 @@ "unavailable_balance_description": "Iwontunws.funfun ti ko si: Lapapọ yii pẹlu awọn owo ti o wa ni titiipa ni awọn iṣowo isunmọ ati awọn ti o ti didi ni itara ninu awọn eto iṣakoso owo rẹ. Awọn iwọntunwọnsi titiipa yoo wa ni kete ti awọn iṣowo oniwun wọn ba ti pari, lakoko ti awọn iwọntunwọnsi tio tutunini ko ni iraye si fun awọn iṣowo titi iwọ o fi pinnu lati mu wọn kuro.", "unconfirmed": "A kò tí ì jẹ́rìí ẹ̀", "understand": "Ó ye mi", + "unlock": "Sisalẹ", "unmatched_currencies": "Irú owó ti àpamọ́wọ́ yín kì í ṣe irú ti yíya àmì ìlujá", "unspent_change": "Yipada", "unspent_coins_details_title": "Àwọn owó ẹyọ t'á kò tí ì san", @@ -839,6 +844,7 @@ "wallet_menu": "Mẹ́nù", "wallet_name": "Orúkọ àpamọ́wọ́", "wallet_name_exists": "Ẹ ti ní àpamọ́wọ́ pẹ̀lú orúkọ̀ yẹn. Ẹ jọ̀wọ́ yàn orúkọ̀ tó yàtọ̀ tàbí pààrọ̀ orúkọ ti àpamọ́wọ́ tẹ́lẹ̀.", + "wallet_password_is_empty": "Ọrọ igbaniwọle apamọwọ ti ṣofo. Ọrọ igbaniwọle apamọwọ ko yẹ ki o ṣofo", "wallet_recovery_height": "Iga Imularada", "wallet_restoration_store_incorrect_seed_length": "Gígùn hóró tí a máa ń lò kọ́ ni èyí", "wallet_seed": "Hóró àpamọ́wọ́", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 89eca2073..5db53a423 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -236,6 +236,7 @@ "enter_code": "输入代码", "enter_seed_phrase": "输入您的种子短语", "enter_totp_code": "请输入 TOTP 代码。", + "enter_wallet_password": "输入钱包密码", "enter_your_note": "输入您的笔记...", "enter_your_pin": "输入密码", "enter_your_pin_again": "再次输入您的PIN码", @@ -340,6 +341,7 @@ "insufficientFundsForRentError": "您没有足够的溶胶来支付该帐户的交易费和租金。请在钱包中添加更多溶胶或减少您发送的溶胶量", "introducing_cake_pay": "介绍 Cake Pay!", "invalid_input": "输入无效", + "invalid_password": "无效的密码", "invoice_details": "发票明细", "is_percentage": "是", "last_30_days": "过去 30 天", @@ -499,6 +501,8 @@ "rename": "重命名", "rep_warning": "代表性警告", "rep_warning_sub": "您的代表似乎并不信誉良好。点击这里选择一个新的", + "repeat_wallet_password": "重复钱包密码", + "repeated_password_is_incorrect": "重复密码不正确。请再次重复钱包密码。", "require_for_adding_contacts": "需要添加联系人", "require_for_all_security_and_backup_settings": "需要所有安全和备份设置", "require_for_assessing_wallet": "需要访问钱包", @@ -797,6 +801,7 @@ "unavailable_balance_description": "不可用余额:此总额包括锁定在待处理交易中的资金以及您在硬币控制设置中主动冻结的资金。一旦各自的交易完成,锁定的余额将变得可用,而冻结的余额在您决定解冻之前仍然无法进行交易。", "unconfirmed": "未确认余额", "understand": "我已知晓", + "unlock": "开锁", "unmatched_currencies": "您当前钱包的货币与扫描的 QR 的货币不匹配", "unspent_change": "改变", "unspent_coins_details_title": "未使用代幣詳情", @@ -838,6 +843,7 @@ "wallet_menu": "钱包菜单", "wallet_name": "钱包名称", "wallet_name_exists": "同名的钱包已经存在", + "wallet_password_is_empty": "钱包密码为空。钱包密码不应为空", "wallet_recovery_height": "恢复高度", "wallet_restoration_store_incorrect_seed_length": "种子长度错误", "wallet_seed": "钱包种子", diff --git a/scripts/android/shell.nix b/scripts/android/shell.nix deleted file mode 100644 index b89da09c0..000000000 --- a/scripts/android/shell.nix +++ /dev/null @@ -1,16 +0,0 @@ -{ pkgs ? import {} }: - -pkgs.mkShell { - buildInputs = [ - pkgs.curl - pkgs.unzip - pkgs.automake - pkgs.file - pkgs.pkg-config - pkgs.git - pkgs.libtool - pkgs.ncurses5 - pkgs.openjdk8 - pkgs.clang - ]; -} diff --git a/scripts/ios/gen_framework.sh b/scripts/ios/gen_framework.sh index 950a7afe5..5c9bcd228 100755 --- a/scripts/ios/gen_framework.sh +++ b/scripts/ios/gen_framework.sh @@ -28,4 +28,4 @@ fi cd $FRWK_DIR # go to iOS framework dir lipo -create $DYLIB_LINK_PATH -output WowneroWallet -echo "Generated ${FRWK_DIR}" \ No newline at end of file +echo "Generated ${FRWK_DIR}" diff --git a/scripts/linux/app_config.sh b/scripts/linux/app_config.sh new file mode 100755 index 000000000..b4ca1423c --- /dev/null +++ b/scripts/linux/app_config.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +CAKEWALLET="cakewallet" +DIR=`pwd` + +if [ -z "$APP_LINUX_TYPE" ]; then + echo "Please set APP_LINUX_TYPE" + exit 1 +fi + +cd ../.. # go to root +CONFIG_ARGS="" + +case $APP_LINUX_TYPE in + $CAKEWALLET) + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --excludeFlutterSecureStorage";; +esac + +cp -rf pubspec_description.yaml pubspec.yaml +flutter pub get +flutter pub run tool/generate_pubspec.dart +flutter pub get +flutter packages pub run tool/configure.dart $CONFIG_ARGS +sed -i '0,/version: 0.0.0/s//version: '"${APP_LINUX_VERSION}"'+'"${APP_LINUX_BUILD_NUMBER}"'/' pubspec.yaml +cd $DIR diff --git a/scripts/linux/app_env.fish b/scripts/linux/app_env.fish new file mode 100644 index 000000000..8dec90ce3 --- /dev/null +++ b/scripts/linux/app_env.fish @@ -0,0 +1,35 @@ +#!/usr/bin/env fish + +set -g APP_LINUX_NAME "" +set -g APP_LINUX_VERSION "" +set -g APP_LINUX_BUILD_NUMBER "" + +set -g CAKEWALLET "cakewallet" + +set -g TYPES $CAKEWALLET +set -g APP_LINUX_TYPE $CAKEWALLET + +if test -n "$argv[1]" + set -g APP_LINUX_TYPE $argv[1] +end + +set -g CAKEWALLET_NAME "Cake Wallet" +set -g CAKEWALLET_VERSION "1.9.0" +set -g CAKEWALLET_BUILD_NUMBER 29 + +if not contains -- $APP_LINUX_TYPE $TYPES + echo "Wrong app type." + exit 1 +end + +switch $APP_LINUX_TYPE + case $CAKEWALLET + set -g APP_LINUX_NAME $CAKEWALLET_NAME + set -g APP_LINUX_VERSION $CAKEWALLET_VERSION + set -g APP_LINUX_BUILD_NUMBER $CAKEWALLET_BUILD_NUMBER +end + +export APP_LINUX_TYPE +export APP_LINUX_NAME +export APP_LINUX_VERSION +export APP_LINUX_BUILD_NUMBER diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh new file mode 100755 index 000000000..729cf376b --- /dev/null +++ b/scripts/linux/app_env.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +APP_LINUX_NAME="" +APP_LINUX_VERSION="" +APP_LINUX_BUILD_VERSION="" + +CAKEWALLET="cakewallet" + +TYPES=($CAKEWALLET) +APP_LINUX_TYPE=$CAKEWALLET + +if [ -n "$1" ]; then + APP_LINUX_TYPE=$1 +fi + +CAKEWALLET_NAME="Cake Wallet" +CAKEWALLET_VERSION="1.9.2" +CAKEWALLET_BUILD_NUMBER=30 + +if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then + echo "Wrong app type." + exit 1 +fi + +case $APP_LINUX_TYPE in + $CAKEWALLET) + APP_LINUX_NAME=$CAKEWALLET_NAME + APP_LINUX_VERSION=$CAKEWALLET_VERSION + APP_LINUX_BUILD_NUMBER=$CAKEWALLET_BUILD_NUMBER;; +esac + +export APP_LINUX_TYPE +export APP_LINUX_NAME +export APP_LINUX_VERSION +export APP_LINUX_BUILD_NUMBER diff --git a/scripts/linux/build_all.sh b/scripts/linux/build_all.sh new file mode 100755 index 000000000..e2bdb081c --- /dev/null +++ b/scripts/linux/build_all.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./build_monero_all.sh diff --git a/scripts/linux/build_boost.sh b/scripts/linux/build_boost.sh new file mode 100755 index 000000000..3ac613e7c --- /dev/null +++ b/scripts/linux/build_boost.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +set -e + +. ./config.sh + +BOOST_SRC_DIR=${EXTERNAL_LINUX_SOURCE_DIR}/boost_1_82_0 +BOOST_FILENAME=boost_1_82_0.tar.bz2 +BOOST_VERSION=1.82.0 +BOOST_FILE_PATH=${EXTERNAL_LINUX_SOURCE_DIR}/$BOOST_FILENAME +BOOST_SHA256="a6e1ab9b0860e6a2881dd7b21fe9f737a095e5f33a3a874afc6a345228597ee6" + +if [ ! -e "$BOOST_FILE_PATH" ]; then + curl -L http://downloads.sourceforge.net/project/boost/boost/${BOOST_VERSION}/${BOOST_FILENAME} > $BOOST_FILE_PATH +fi + +echo $BOOST_SHA256 $BOOST_FILE_PATH | sha256sum -c - || exit 1 + +cd $EXTERNAL_LINUX_SOURCE_DIR +rm -rf $BOOST_SRC_DIR +tar -xvf $BOOST_FILE_PATH -C $EXTERNAL_LINUX_SOURCE_DIR +cd $BOOST_SRC_DIR +./bootstrap.sh --prefix=${EXTERNAL_LINUX_DIR} +./b2 cxxflags=-fPIC cflags=-fPIC \ + --with-chrono \ + --with-date_time \ + --with-filesystem \ + --with-program_options \ + --with-regex \ + --with-serialization \ + --with-system \ + --with-thread \ + --with-locale \ + link=static \ + install + diff --git a/scripts/linux/build_expat.sh b/scripts/linux/build_expat.sh new file mode 100755 index 000000000..a45852d1d --- /dev/null +++ b/scripts/linux/build_expat.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +set -e + +. ./config.sh + + +EXPAT_VERSION=R_2_4_8 +EXPAT_HASH="3bab6c09bbe8bf42d84b81563ddbcf4cca4be838" +EXPAT_SRC_DIR=${EXTERNAL_LINUX_SOURCE_DIR}/libexpat + +git clone https://github.com/libexpat/libexpat.git -b ${EXPAT_VERSION} ${EXPAT_SRC_DIR} +cd $EXPAT_SRC_DIR +test `git rev-parse HEAD` = ${EXPAT_HASH} || exit 1 +cd $EXPAT_SRC_DIR/expat + +./buildconf.sh +./configure --enable-static --disable-shared --prefix=${EXTERNAL_LINUX_DIR} +make +make install diff --git a/scripts/linux/build_iconv.sh b/scripts/linux/build_iconv.sh new file mode 100755 index 000000000..29812cdb3 --- /dev/null +++ b/scripts/linux/build_iconv.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -e + +. ./config.sh + +export ICONV_FILENAME=libiconv-1.16.tar.gz +export ICONV_FILE_PATH=${EXTERNAL_LINUX_SOURCE_DIR}/${ICONV_FILENAME} +export ICONV_SRC_DIR=${EXTERNAL_LINUX_SOURCE_DIR}/libiconv-1.16 +ICONV_SHA256="e6a1b1b589654277ee790cce3734f07876ac4ccfaecbee8afa0b649cf529cc04" + +curl http://ftp.gnu.org/pub/gnu/libiconv/${ICONV_FILENAME} -o $ICONV_FILE_PATH +echo $ICONV_SHA256 $ICONV_FILE_PATH | sha256sum -c - || exit 1 + +cd $EXTERNAL_LINUX_SOURCE_DIR +rm -rf $ICONV_SRC_DIR +tar -xzf $ICONV_FILE_PATH -C $EXTERNAL_LINUX_SOURCE_DIR +cd $ICONV_SRC_DIR + +./configure --prefix=${EXTERNAL_LINUX_DIR} +make +make install diff --git a/scripts/linux/build_monero.sh b/scripts/linux/build_monero.sh new file mode 100755 index 000000000..cbefec08e --- /dev/null +++ b/scripts/linux/build_monero.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +. ./config.sh + +MONERO_URL="https://github.com/cake-tech/monero.git" +MONERO_DIR_PATH="${EXTERNAL_LINUX_SOURCE_DIR}/monero" +MONERO_VERSION=release-v0.18.3.2 +PREFIX=${EXTERNAL_LINUX_DIR} +DEST_LIB_DIR=${EXTERNAL_LINUX_LIB_DIR}/monero +DEST_INCLUDE_DIR=${EXTERNAL_LINUX_INCLUDE_DIR}/monero + +echo "Cloning monero from - $MONERO_URL to - $MONERO_DIR_PATH" +git clone $MONERO_URL $MONERO_DIR_PATH +cd $MONERO_DIR_PATH +git checkout $MONERO_VERSION +git submodule update --init --force +rm -rf ./build/release +mkdir -p ./build/release +cd ./build/release + +mkdir -p $DEST_LIB_DIR +mkdir -p $DEST_INCLUDE_DIR + +echo "Building LINUX" +export CMAKE_INCLUDE_PATH="${PREFIX}/include" +export CMAKE_LIBRARY_PATH="${PREFIX}/lib" + +cmake -DSTATIC=ON \ + -DBUILD_GUI_DEPS=ON \ + -DUNBOUND_INCLUDE_DIR=${EXTERNAL_LINUX_INCLUDE_DIR} \ + -DCMAKE_INSTALL_PREFIX=${PREFIX} \ + -DUSE_DEVICE_TREZOR=OFF \ + -DMANUAL_SUBMODULES=1 \ + ../.. + +make wallet_api -j$(($(nproc) / 2)) + +find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; +cp -r ./lib/* $DEST_LIB_DIR +cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR diff --git a/scripts/linux/build_monero_all.sh b/scripts/linux/build_monero_all.sh new file mode 100755 index 000000000..5dc512527 --- /dev/null +++ b/scripts/linux/build_monero_all.sh @@ -0,0 +1,21 @@ +#!/bin/bash + + +. ./config.sh + + +set -x -e + +cd "$(dirname "$0")" + +NPROC="-j$(nproc)" + +../prepare_moneroc.sh + +for COIN in monero wownero; +do + pushd ../monero_c + ./build_single.sh ${COIN} $(gcc -dumpmachine) $NPROC + popd + unxz -f ../monero_c/release/${COIN}/$(gcc -dumpmachine)_libwallet2_api_c.so.xz +done diff --git a/scripts/linux/build_openssl.sh b/scripts/linux/build_openssl.sh new file mode 100755 index 000000000..205cf7abf --- /dev/null +++ b/scripts/linux/build_openssl.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e + +. ./config.sh + +OPENSSL_FILENAME=openssl-1.1.1q.tar.gz +OPENSSL_FILE_PATH=${EXTERNAL_LINUX_SOURCE_DIR}/${OPENSSL_FILENAME} +OPENSSL_SRC_DIR=${EXTERNAL_LINUX_SOURCE_DIR}/openssl-1.1.1q +OPENSSL_SHA256="d7939ce614029cdff0b6c20f0e2e5703158a489a72b2507b8bd51bf8c8fd10ca" + +curl https://www.openssl.org/source/${OPENSSL_FILENAME} -o ${OPENSSL_FILE_PATH} + +rm -rf $OPENSSL_SRC_DIR +tar -xzf $OPENSSL_FILE_PATH -C $EXTERNAL_LINUX_SOURCE_DIR +cd $OPENSSL_SRC_DIR +export CFLAGS=-fPIC +./config -fPIC shared --prefix=${EXTERNAL_LINUX_DIR} +make install diff --git a/scripts/linux/build_sodium.sh b/scripts/linux/build_sodium.sh new file mode 100755 index 000000000..3a6f6adf9 --- /dev/null +++ b/scripts/linux/build_sodium.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e + +. ./config.sh + +SODIUM_PATH="${EXTERNAL_LINUX_SOURCE_DIR}/libsodium" +SODIUM_URL="https://github.com/jedisct1/libsodium.git" + +echo "============================ SODIUM ============================" + +echo "Cloning SODIUM from - $SODIUM_URL" +git clone $SODIUM_URL $SODIUM_PATH --branch stable +cd $SODIUM_PATH + + +./configure --prefix=${EXTERNAL_LINUX_DIR} +make +make install diff --git a/scripts/linux/build_unbound.sh b/scripts/linux/build_unbound.sh new file mode 100755 index 000000000..1ae917da9 --- /dev/null +++ b/scripts/linux/build_unbound.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +. ./config.sh + +UNBOUND_VERSION=release-1.16.2 +UNBOUND_HASH="cbed768b8ff9bfcf11089a5f1699b7e5707f1ea5" +UNBOUND_DIR_PATH="${EXTERNAL_LINUX_SOURCE_DIR}/unbound-1.16.2" + +echo "============================ Unbound ============================" +rm -rf ${UNBOUND_DIR_PATH} +git clone https://github.com/NLnetLabs/unbound.git -b ${UNBOUND_VERSION} ${UNBOUND_DIR_PATH} +cd $UNBOUND_DIR_PATH +test `git rev-parse HEAD` = ${UNBOUND_HASH} || exit 1 + +export CFLAGS=-fPIC +./configure cxxflags=-fPIC cflags=-fPIC \ + --prefix="${EXTERNAL_LINUX_DIR}" \ + --with-ssl="${EXTERNAL_LINUX_DIR}" \ + --with-libexpat="${EXTERNAL_LINUX_DIR}" \ + --enable-static \ + --disable-flto +make +make install diff --git a/scripts/linux/build_zmq.sh b/scripts/linux/build_zmq.sh new file mode 100755 index 000000000..f6980e40d --- /dev/null +++ b/scripts/linux/build_zmq.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +. ./config.sh + +ZMQ_PATH="${EXTERNAL_LINUX_SOURCE_DIR}/libzmq" +ZMQ_URL="https://github.com/zeromq/libzmq.git" + +echo "============================ ZMQ ============================" + +echo "Cloning ZMQ from - $ZMQ_URL" +git clone $ZMQ_URL $ZMQ_PATH +cd $ZMQ_PATH +mkdir cmake-build +cd cmake-build +cmake .. -DCMAKE_INSTALL_PREFIX="${EXTERNAL_LINUX_DIR}" +make +make install diff --git a/scripts/linux/cakewallet.sh b/scripts/linux/cakewallet.sh new file mode 100755 index 000000000..89c7a7ef0 --- /dev/null +++ b/scripts/linux/cakewallet.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +. ./app_env.sh "cakewallet" +. ./app_config.sh diff --git a/scripts/linux/config.sh b/scripts/linux/config.sh new file mode 100755 index 000000000..3fbdf349e --- /dev/null +++ b/scripts/linux/config.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +export LINUX_SCRIPTS_DIR=`pwd` +export CW_ROOT=${LINUX_SCRIPTS_DIR}/../.. +export EXTERNAL_DIR=${CW_ROOT}/cw_shared_external/ios/External +export EXTERNAL_LINUX_DIR=${EXTERNAL_DIR}/linux +export EXTERNAL_LINUX_SOURCE_DIR=${EXTERNAL_LINUX_DIR}/sources +export EXTERNAL_LINUX_LIB_DIR=${EXTERNAL_LINUX_DIR}/lib +export EXTERNAL_LINUX_INCLUDE_DIR=${EXTERNAL_LINUX_DIR}/include + +mkdir -p $EXTERNAL_LINUX_LIB_DIR +mkdir -p $EXTERNAL_LINUX_INCLUDE_DIR +mkdir -p $EXTERNAL_LINUX_SOURCE_DIR diff --git a/scripts/linux/gcc10.nix b/scripts/linux/gcc10.nix new file mode 100644 index 000000000..dfc01986a --- /dev/null +++ b/scripts/linux/gcc10.nix @@ -0,0 +1,12 @@ +with import {}; +gcc10Stdenv.mkDerivation { + name="gcc10-stdenv"; + buildInputs = [ + pkgs.cmake + pkgs.pkg-config + pkgs.autoconf + pkgs.libtool + pkgs.expat + ]; +} + diff --git a/scripts/linux/setup.sh b/scripts/linux/setup.sh new file mode 100755 index 000000000..a323cf027 --- /dev/null +++ b/scripts/linux/setup.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +. ./config.sh + +CW_EXTERNAL_DIR=${CW_ROOT}/cw_monero/ios/External/linux +CW_EXTERNAL_DIR_INCLUDE=${CW_EXTERNAL_DIR}/include + +mkdir -p $CW_EXTERNAL_DIR_INCLUDE +cp $EXTERNAL_LINUX_INCLUDE_DIR/monero/wallet2_api.h $CW_EXTERNAL_DIR_INCLUDE diff --git a/tool/configure.dart b/tool/configure.dart index c37946476..a0104c34e 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -150,7 +150,7 @@ abstract class Bitcoin { String? passphrase, }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); - WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo}); + WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); List getWordList(); Map getWalletKeys(Object wallet); @@ -179,8 +179,8 @@ abstract class Bitcoin { List getUnspents(Object wallet); Future updateUnspents(Object wallet); WalletService createBitcoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool alwaysScan); - WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource); + Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); + WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource, bool isDirect); TransactionPriority getBitcoinTransactionPriorityMedium(); TransactionPriority getBitcoinTransactionPriorityCustom(); TransactionPriority getLitecoinTransactionPriorityMedium(); @@ -369,7 +369,7 @@ abstract class Monero { required String language, required int height}); WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); - WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String password}); + WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String? password}); Map getKeys(Object wallet); int? getRestoreHeight(Object wallet); Object createMoneroTransactionCreationCredentials({required List outputs, required TransactionPriority priority}); @@ -734,7 +734,7 @@ abstract class Haven { required String language, required int height}); WalletCredentials createHavenRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); - WalletCredentials createHavenNewWalletCredentials({required String name, required String language, String password}); + WalletCredentials createHavenNewWalletCredentials({required String name, required String language, String? password}); Map getKeys(Object wallet); Object createHavenTransactionCreationCredentials({required List outputs, required TransactionPriority priority, required String assetType}); String formatterMoneroAmountToString({required int amount}); @@ -828,8 +828,8 @@ import 'package:eth_sig_util/util/utils.dart'; const ethereumContent = """ abstract class Ethereum { List getEthereumWordList(String language); - WalletService createEthereumWalletService(Box walletInfoSource); - WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo}); + WalletService createEthereumWalletService(Box walletInfoSource, bool isDirect); + WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createEthereumRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createEthereumHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); @@ -932,8 +932,8 @@ import 'package:eth_sig_util/util/utils.dart'; const polygonContent = """ abstract class Polygon { List getPolygonWordList(String language); - WalletService createPolygonWalletService(Box walletInfoSource); - WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo}); + WalletService createPolygonWalletService(Box walletInfoSource, bool isDirect); + WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createPolygonRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createPolygonHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); @@ -1017,10 +1017,10 @@ abstract class BitcoinCash { String getCashAddrFormat(String address); WalletService createBitcoinCashWalletService( - Box walletInfoSource, Box unspentCoinSource); + Box walletInfoSource, Box unspentCoinSource, bool isDirect); WalletCredentials createBitcoinCashNewWalletCredentials( - {required String name, WalletInfo? walletInfo}); + {required String name, WalletInfo? walletInfo, String? password}); WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( {required String name, required String mnemonic, required String password}); @@ -1097,11 +1097,11 @@ abstract class Nano { void setCurrentAccount(Object wallet, int id, String label, String? balance); - WalletService createNanoWalletService(Box walletInfoSource); + WalletService createNanoWalletService(Box walletInfoSource, bool isDirect); WalletCredentials createNanoNewWalletCredentials({ required String name, - String password, + String? password, }); WalletCredentials createNanoRestoreWalletFromSeedCredentials({ @@ -1215,9 +1215,9 @@ import 'package:cw_solana/solana_wallet_creation_credentials.dart'; const solanaContent = """ abstract class Solana { List getSolanaWordList(String language); - WalletService createSolanaWalletService(Box walletInfoSource); + WalletService createSolanaWalletService(Box walletInfoSource, bool isDirect); WalletCredentials createSolanaNewWalletCredentials( - {required String name, WalletInfo? walletInfo}); + {required String name, WalletInfo? walletInfo, String? password}); WalletCredentials createSolanaRestoreWalletFromSeedCredentials( {required String name, required String mnemonic, required String password}); WalletCredentials createSolanaRestoreWalletFromPrivateKey( @@ -1302,8 +1302,8 @@ import 'package:cw_tron/tron_wallet_service.dart'; const tronContent = """ abstract class Tron { List getTronWordList(String language); - WalletService createTronWalletService(Box walletInfoSource); - WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo}); + WalletService createTronWalletService(Box walletInfoSource, bool isDirect); + WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createTronRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); String getAddress(WalletBase wallet); From 0491ad9ee24175240556ad4bb63634b52d78f0b2 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Tue, 13 Aug 2024 07:15:47 -0400 Subject: [PATCH 023/142] Update support links (#1595) * Update support links list, add proper support for light/dark icons, update provider icons * Update trocador icon * Update variable * trial fix for android build --------- Co-authored-by: OmarHatem --- android/build.gradle | 4 +- assets/images/dfx_dark.png | Bin 3515 -> 10166 bytes assets/images/dfx_light.png | Bin 3502 -> 12024 bytes assets/images/moonpay.png | Bin 1392 -> 16575 bytes assets/images/moonpay_dark.png | Bin 23214 -> 14212 bytes assets/images/moonpay_light.png | Bin 20800 -> 13866 bytes assets/images/trocador.png | Bin 10417 -> 28127 bytes cw_haven/android/build.gradle | 4 +- cw_shared_external/android/build.gradle | 4 +- .../support_other_links_page.dart | 10 ++-- lib/view_model/settings/link_list_item.dart | 2 + lib/view_model/support_view_model.dart | 47 ++++++++++++------ 12 files changed, 47 insertions(+), 24 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index aa9f5005d..7ddb75179 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = '1.8.21' repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,7 +15,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/assets/images/dfx_dark.png b/assets/images/dfx_dark.png index cbba87372adad4c0c3f5d14dec996aacea01ee88..6ac112eae3be1f20e7195d60fd0bb8ef357e2d70 100644 GIT binary patch literal 10166 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 diff --git a/assets/images/dfx_light.png b/assets/images/dfx_light.png index e4836be3ebecc08d3d0dee8fe3af672c511c7c2f..a045d3e681c7f9648178a5a887652ba7de507105 100644 GIT binary patch literal 12024 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 literal 3502 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%0D6h@cbjYa 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 Q3^X{UW1#)&g!P^O1qB%bP5=M^ literal 1392 zcmeAS@N?(olHy`uVBq!ia0vp^;y^6H!3HD+Ytl-A6id3JuOkD)#(wTUiL5}rLb6AY zF9SoB8UsT^3j@P1pisjL28L1t28LG&3=CE?fMyiT*%fF5l&DCJ@J#ddWzYh$IT%l<#Nv5>Skcg59UmvUF{9L`nl>DSry^7od zkS+$B3M(KpH?<^Dp&~aYuh^=>Rtc=a3djZt>nkaMm6T-LDnNc-DA*LGq*(>IxIwi8dA3R!B_#z``ugSN<$C4Ddih1^`i7R4mih)p`bI{&Koz>h zm3bwJ6}oxF$`C_f=D4I5Cl_TFlw{`TDS*sPOv*1Uu~kw6Sp)|Vca~(PA#BPkhI$L= zL4A;nzM-ChJ~nNs6`44+fn*@s!2W_*X9F_K%D*Tx73g4)v+N9Qz!sp0A)E(MACy|0 zpHm7_9-5a~VrK-^f+mcl3uL!dKxRd1PNYj_ZfagJ$R=Ym8-1)2ST#lgGGVyZpUS#4^KmOT)on|-+^iNgQtsQ zhzIZ0se8Sg9R*t7n_lc_T`-M}MS9~UCy_O}wG11#Y~Q$L(hdb>%CV`(^O|I!11l9{%%(Ngec}WY&JVI zx}u)l=5yTt)c2A1YqgS4w%rFz+wL=1^>IZ^61tbUCA3-PsI9=PGQ}56d*=$Il=Pkt#V$_5brG_s_9#UlhK; z@7=+v6F0x#vUbs|zO`&N_MZ=OS(o4EI^piH>}bil%*h3ZCmlEW=90{6Izz3jM{(!T z59|kK$N6&1w^x4gSN5Z}!?)>6;&1ZpV9s1U(Sjv;Ry%+B`=xXK3xAG0acZM?Ilu9d z*9nXsZv+6e1p86qv YGw80q^r|1g;*f#C)78&qol`;+0McLD?*IS* diff --git a/assets/images/moonpay_dark.png b/assets/images/moonpay_dark.png index 872e322e2a5f8ab6a639f5949dff97ffcd325e00..21de98eb4389c69e334e252a65652e2c79c06255 100644 GIT binary patch literal 14212 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 NZZq3mcTA|K{ukylw<`bu literal 23214 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 diff --git a/assets/images/moonpay_light.png b/assets/images/moonpay_light.png index c76ae6e74ec7e578874528b4c670ca4829be5647..3d3de2e4f17670fec4f60e869ad7b8151e37e202 100644 GIT binary patch literal 13866 zcmb7rc|6oz^#2{h7#R$nGG%+Dv=Bqtk|?4DX=9XShA3-f%RXhIvb0zdp^X$VDzcl# z_B;|rmTZX!St3cw*6)1uJiqVv_51Jl$Gm*bUCurC+;h&o_kFI*=4Qr1E7q()2np@m zYj_wTJOld?2~gredZq_|1U&azc_Sn!j{R^b`8EX#ao&fGcca`!sSzM}Pwg_@g;4%g z!372%Lh6u20=3X6^2Z_7J!tn_j3Y(Sg$yTeVq zMe9@Z+B#h;#SrRKNY0h94h$LKwkucIE}cvFYpQE1otqe+`T2hM)!4{;MZquo5n`r_ z<;tWTi`#ipcgOrfxXBrb+6q(IH)is8UdNINh;v-s#=SW*N3(HfuH5OKn%ol>-1pzd z2Q-fEBjOQ#i^d4PJ?NHyU&$k}hC-W=gq`v31}j-qgc2X~=9*eo*8kl5G5K?equo&1 z3DBG8sr4t_OXi+uPnOQ51Ul=EjqbC&$^+#F>(5F1&TPBT9x^ul>{p`eBn3)uj&HX; zD1S~!`Y(`kWmk39$Btu5C-k)E&xjzDqdyX;J29Hlpjf7qpO!HhtBylx)hThCyyUTc zb1`uUaT?s4(`S+m$cGkxSr|jDQ%1DZlEz|Dm(+#G!2Gm56g*;%er^m}t^G$WjDQoc zw+MQsHTDzlCY zU0@5CJp4Sm_^jP3NF6j-AW}M~z(Z+qZT@v)M}5QMT7=ji26A_P9xc$9uCJ^yJ|!Er z5+T~9v)_lN8^#*09k)veyZ>y*h9MIWQ$pQqDeC9xtySLkjVv5uPg8j))6FL%emPx) z#k_L1(Fi%jCVvyuk#ST|8baufH78h1P%A0#>zP?io5eS+?T>DQxv)L1md>?v)Pg;~ zAD1z?>MhiY%QH`->C>~~WTXO2kEc?p#zzbm6qZL%9w zu2`-Ngvz&2nf$vl94fJ%#O2Db0(A33WN|Lo+7s*QaT)3-_rotQHspU=e4JHC{}ZX1Eiuxi_Tjk@D1XH=?~`NQokP?~=czEUy;p ztNB1d?ovEX00fx!eb-t^(0brsI(IiaZ!3A=5|q8G)}~VG?+jOgXE6f;R}FLMd%JRb zn^3eo4{A7AEHZwGDW^V?V$mg)WCqhM+cr{4=V;Z_`kjUMf}bWyI9#IUig9}X#;rEy zMM)w+R)jU2uM_hH^$p#p=WfqE7G@{3j17s<^>Zk zQ3uAR%tr|aZhzPjRK7J8=ZotK4WqPy_GRxu zoL#z&xi!hOy@2tL1)gpr5OERZtZsZ>IkrIMPt=h>Lezk! z`0_&`>~F2Li1S>=Z^o3Fajr7^^Q)x`n0B9gguGQWFEDOjVhHR***evL;VHt7=TbJ5H%=M*bcST~lbq-En z8Yi+%ER{dKff-Of9O9+YO1|n=yr1R694WZHSDsgiTQ|Sc88aU1!I2-cxHkIa-g}r( zMLsX-y7dM(B`Su&W0_t847ReY?Gg*)?6tUslUzoch74zX3SFL)j!{YRqP>mayVxk?y>8~|PMX-Ke3!6E~ zi{G|#Zec`ldgwX<+Qan5i`Wv9&WPhkZMh4ij`-!aA9YEb_f0(5B+jLv+lxxe?s2X$ z99Q$9>35Ip%ZTu;zUhJyXoZqEP3P>9#G2Efc@Q=!CQ+qQM9=@O%3mpZ?)+!)sH&eQkn&qh%Mr>H z_D!r630jq7Gh=*W;es$%tUArZj$E!vrP5U}aVWe0Z5*wDf|dvS;fTJI!G@60I!wy@ z0vs8L28eSBI3cEJIc&rcUtXZ`yP$eRT?2xDKll&8zSw0a-QH$XQ;VqpX3(F1BXh+~J0 zMAPk-9$<5;(Bq|rmrEXs0aZme?Om&6H^MBhm=s)Yt9YhS=@Zkj^sB8tW=(69JtBbE z6@~E~32teHu+!o{Z>+m*`ĨI26f69J26q~Q|*@hc>{Q5v! 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.iconColor; + final isLightMode = Theme.of(context).extension()?.useDarkImage ?? false; + return Container( child: Center( child: ConstrainedBox( @@ -37,16 +41,16 @@ class SupportOtherLinksPage extends BasePage { if (item is RegularListItem) { return SettingsCellWithArrow(title: item.title, handler: item.handler); } - if (item is LinkListItem) { + bool hasLightIcon = false; + if (item.lightIcon != null) hasLightIcon = true; return SettingsLinkProviderCell( title: item.title, - icon: item.icon, + icon: isLightMode && hasLightIcon ? item.lightIcon : item.icon, iconColor: item.hasIconColor ? iconColor : null, link: item.link, linkTitle: item.linkTitle); } - return Container(); }), ), diff --git a/lib/view_model/settings/link_list_item.dart b/lib/view_model/settings/link_list_item.dart index 4ee4162a9..8d7607eb5 100644 --- a/lib/view_model/settings/link_list_item.dart +++ b/lib/view_model/settings/link_list_item.dart @@ -8,10 +8,12 @@ class LinkListItem extends SettingsListItem { required this.link, required this.linkTitle, this.icon, + this.lightIcon, this.hasIconColor = false}) : super(title); final String? icon; + final String? lightIcon; final String link; final String linkTitle; final bool hasIconColor; diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index ccef76154..2bb749b42 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -33,11 +33,6 @@ abstract class SupportViewModelBase with Store { icon: 'assets/images/Telegram.png', linkTitle: '@cakewallet_bot', link: 'https://t.me/cakewallet_bot'), - LinkListItem( - title: 'Twitter', - icon: 'assets/images/Twitter.png', - linkTitle: '@cakewallet', - link: 'https://twitter.com/cakewallet'), LinkListItem( title: 'ChangeNow', icon: 'assets/images/change_now.png', @@ -46,7 +41,7 @@ abstract class SupportViewModelBase with Store { LinkListItem( title: 'SideShift', icon: 'assets/images/sideshift.png', - linkTitle: S.current.help, + linkTitle: 'help.sideshift.ai', link: 'https://help.sideshift.ai/en/'), LinkListItem( title: 'SimpleSwap', @@ -58,19 +53,41 @@ abstract class SupportViewModelBase with Store { icon: 'assets/images/exolix.png', linkTitle: 'support@exolix.com', link: 'mailto:support@exolix.com'), - if (!isMoneroOnly) ... [ - LinkListItem( - title: 'Wyre', - icon: 'assets/images/wyre.png', - linkTitle: S.current.submit_request, - link: 'https://wyre-support.zendesk.com/hc/en-us/requests/new'), + LinkListItem( + title: 'Quantex', + icon: 'assets/images/quantex.png', + linkTitle: 'help.myquantex.com', + link: 'mailto:support@exolix.com'), + LinkListItem( + title: 'Trocador', + icon: 'assets/images/trocador.png', + linkTitle: 'mail@trocador.app', + link: 'mailto:mail@trocador.app'), + LinkListItem( + title: 'Onramper', + icon: 'assets/images/onramper_dark.png', + lightIcon: 'assets/images/onramper_light.png', + linkTitle: 'View exchanges', + link: 'https://guides.cakewallet.com/docs/service-support/buy/#onramper'), + LinkListItem( + title: 'DFX', + icon: 'assets/images/dfx_dark.png', + lightIcon: 'assets/images/dfx_light.png', + linkTitle: 'support@dfx.swiss', + link: 'mailto:support@dfx.swiss'), + if (!isMoneroOnly) ... [ LinkListItem( title: 'MoonPay', icon: 'assets/images/moonpay.png', - hasIconColor: true, linkTitle: S.current.submit_request, - link: 'https://support.moonpay.com/hc/en-gb/requests/new') - ] + link: 'https://support.moonpay.com/hc/en-gb/requests/new'), + LinkListItem( + title: 'Robinhood Connect', + icon: 'assets/images/robinhood_dark.png', + lightIcon: 'assets/images/robinhood_light.png', + linkTitle: S.current.submit_request, + link: 'https://robinhood.com/contact') + ] //LinkListItem( // title: 'Yat', // icon: 'assets/images/yat_mini_logo.png', From 29492998216ec65a5b986a8ca168c97a2b68f727 Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 13 Aug 2024 14:15:31 +0200 Subject: [PATCH 024/142] unstoppable domains fix (#1600) --- android/app/build.gradle | 1 - .../cakewallet/cake_wallet/MainActivity.java | 29 ------------------- .../com/cakewallet/haven/MainActivity.java | 29 ------------------- .../java/com/monero/app/MainActivity.java | 29 ------------------- ios/Podfile | 1 - ios/Runner/AppDelegate.swift | 23 +-------------- lib/entities/unstoppable_domain_address.dart | 27 +++++++++-------- 7 files changed, 16 insertions(+), 123 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 60defb1fd..2f5427531 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,5 +91,4 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - implementation 'com.unstoppabledomains:resolution:5.0.0' } diff --git a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java index 29b37c46c..df3f6be01 100644 --- a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java +++ b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java @@ -20,14 +20,10 @@ import android.net.Uri; import android.os.PowerManager; import android.provider.Settings; -import com.unstoppabledomains.resolution.DomainResolution; -import com.unstoppabledomains.resolution.Resolution; - import java.security.SecureRandom; public class MainActivity extends FlutterFragmentActivity { final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; - final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24; boolean isAppSecure = false; @Override @@ -53,14 +49,6 @@ public class MainActivity extends FlutterFragmentActivity { random.nextBytes(bytes); handler.post(() -> result.success(bytes)); break; - case "getUnstoppableDomainAddress": - int version = Build.VERSION.SDK_INT; - if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) { - getUnstoppableDomainAddress(call, result); - } else { - handler.post(() -> result.success("")); - } - break; case "setIsAppSecure": isAppSecure = call.argument("isAppSecure"); if (isAppSecure) { @@ -85,23 +73,6 @@ public class MainActivity extends FlutterFragmentActivity { } } - private void getUnstoppableDomainAddress(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - DomainResolution resolution = new Resolution(); - Handler handler = new Handler(Looper.getMainLooper()); - String domain = call.argument("domain"); - String ticker = call.argument("ticker"); - - AsyncTask.execute(() -> { - try { - String address = resolution.getAddress(domain, ticker); - handler.post(() -> result.success(address)); - } catch (Exception e) { - System.out.println("Expected Address, but got " + e.getMessage()); - handler.post(() -> result.success("")); - } - }); - } - private void disableBatteryOptimization() { String packageName = getPackageName(); PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); diff --git a/android/app/src/main/java/com/cakewallet/haven/MainActivity.java b/android/app/src/main/java/com/cakewallet/haven/MainActivity.java index d0a465d22..83a790683 100644 --- a/android/app/src/main/java/com/cakewallet/haven/MainActivity.java +++ b/android/app/src/main/java/com/cakewallet/haven/MainActivity.java @@ -19,14 +19,10 @@ import android.net.Uri; import android.os.PowerManager; import android.provider.Settings; -import com.unstoppabledomains.resolution.DomainResolution; -import com.unstoppabledomains.resolution.Resolution; - import java.security.SecureRandom; public class MainActivity extends FlutterFragmentActivity { final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; - final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24; @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { @@ -51,14 +47,6 @@ public class MainActivity extends FlutterFragmentActivity { random.nextBytes(bytes); handler.post(() -> result.success(bytes)); break; - case "getUnstoppableDomainAddress": - int version = Build.VERSION.SDK_INT; - if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) { - getUnstoppableDomainAddress(call, result); - } else { - handler.post(() -> result.success("")); - } - break; case "disableBatteryOptimization": disableBatteryOptimization(); handler.post(() -> result.success(null)); @@ -75,23 +63,6 @@ public class MainActivity extends FlutterFragmentActivity { } } - private void getUnstoppableDomainAddress(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - DomainResolution resolution = new Resolution(); - Handler handler = new Handler(Looper.getMainLooper()); - String domain = call.argument("domain"); - String ticker = call.argument("ticker"); - - AsyncTask.execute(() -> { - try { - String address = resolution.getAddress(domain, ticker); - handler.post(() -> result.success(address)); - } catch (Exception e) { - System.out.println("Expected Address, but got " + e.getMessage()); - handler.post(() -> result.success("")); - } - }); - } - private void disableBatteryOptimization() { String packageName = getPackageName(); PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); diff --git a/android/app/src/main/java/com/monero/app/MainActivity.java b/android/app/src/main/java/com/monero/app/MainActivity.java index 49c368ec7..e6306d27b 100644 --- a/android/app/src/main/java/com/monero/app/MainActivity.java +++ b/android/app/src/main/java/com/monero/app/MainActivity.java @@ -19,14 +19,10 @@ import android.net.Uri; import android.os.PowerManager; import android.provider.Settings; -import com.unstoppabledomains.resolution.DomainResolution; -import com.unstoppabledomains.resolution.Resolution; - import java.security.SecureRandom; public class MainActivity extends FlutterFragmentActivity { final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; - final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24; boolean isAppSecure = false; @Override @@ -52,14 +48,6 @@ public class MainActivity extends FlutterFragmentActivity { random.nextBytes(bytes); handler.post(() -> result.success(bytes)); break; - case "getUnstoppableDomainAddress": - int version = Build.VERSION.SDK_INT; - if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) { - getUnstoppableDomainAddress(call, result); - } else { - handler.post(() -> result.success("")); - } - break; case "setIsAppSecure": isAppSecure = call.argument("isAppSecure"); if (isAppSecure) { @@ -84,23 +72,6 @@ public class MainActivity extends FlutterFragmentActivity { } } - private void getUnstoppableDomainAddress(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - DomainResolution resolution = new Resolution(); - Handler handler = new Handler(Looper.getMainLooper()); - String domain = call.argument("domain"); - String ticker = call.argument("ticker"); - - AsyncTask.execute(() -> { - try { - String address = resolution.getAddress(domain, ticker); - handler.post(() -> result.success(address)); - } catch (Exception e) { - System.out.println("Expected Address, but got " + e.getMessage()); - handler.post(() -> result.success("")); - } - }); - } - private void disableBatteryOptimization() { String packageName = getPackageName(); PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); diff --git a/ios/Podfile b/ios/Podfile index 51622ff10..f0a0721a6 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -36,7 +36,6 @@ target 'Runner' do # Cake Wallet (Legacy) pod 'CryptoSwift' - pod 'UnstoppableDomainsResolution', '~> 4.0.0' end post_install do |installer| diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index acdfa4346..0cc4eebe8 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,6 +1,5 @@ import UIKit import Flutter -import UnstoppableDomainsResolution import workmanager @UIApplicationMain @@ -87,27 +86,7 @@ import workmanager } result(secRandom(count: count)) - case "getUnstoppableDomainAddress": - guard let args = call.arguments as? Dictionary, - let domain = args["domain"], - let ticker = args["ticker"], - let resolution = self?.resolution else { - result(nil) - return - } - - resolution.addr(domain: domain, ticker: ticker) { addrResult in - var address : String = "" - - switch addrResult { - case .success(let returnValue): - address = returnValue - case .failure(let error): - print("Expected Address, but got \(error)") - } - - result(address) - } + case "setIsAppSecure": guard let args = call.arguments as? Dictionary, let isAppSecure = args["isAppSecure"] else { diff --git a/lib/entities/unstoppable_domain_address.dart b/lib/entities/unstoppable_domain_address.dart index c5ec71ab5..0f56517b8 100644 --- a/lib/entities/unstoppable_domain_address.dart +++ b/lib/entities/unstoppable_domain_address.dart @@ -1,5 +1,7 @@ -import 'package:cake_wallet/utils/device_info.dart'; +import 'dart:convert'; + import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; const channel = MethodChannel('com.cake_wallet/native_utils'); @@ -7,18 +9,19 @@ Future fetchUnstoppableDomainAddress(String domain, String ticker) async var address = ''; try { - if (DeviceInfo.instance.isMobile) { - address = await channel.invokeMethod( - 'getUnstoppableDomainAddress', - { - 'domain' : domain, - 'ticker' : ticker - } - ) ?? ''; - } else { - // TODO: Integrate with Unstoppable domains resolution API - return address; + final uri = Uri.parse("https://api.unstoppabledomains.com/profile/public/${Uri.encodeQueryComponent(domain)}?fields=records"); + final jsonString = await http.read(uri); + final jsonParsed = json.decode(jsonString) as Map; + if (jsonParsed["records"] == null) { + throw Exception(".records response from $uri is empty"); + }; + final records = jsonParsed["records"] as Map; + final key = "crypto.${ticker.toUpperCase()}.address"; + if (records[key] == null) { + throw Exception(".records.${key} response from $uri is empty"); } + + return records[key] as String? ?? ''; } catch (e) { print('Unstoppable domain error: ${e.toString()}'); address = ''; From 4b69277858a75ea3beafb268eb9f4c60cef9f070 Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 13 Aug 2024 15:47:37 +0200 Subject: [PATCH 025/142] add mutex around _confirmForm to prevent the wallets from breaking (#1602) * add mutex around _confirmForm to prevent the wallets from breaking * add async * drop mutex for a boolean * don't make the variable global --- .../screens/new_wallet/new_wallet_page.dart | 49 +++--- .../screens/restore/wallet_restore_page.dart | 139 ++++++++++-------- 2 files changed, 106 insertions(+), 82 deletions(-) diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index b66aab4cf..cd5a7ce8d 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -25,6 +25,7 @@ import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/entities/seed_type.dart'; + class NewWalletPage extends BasePage { NewWalletPage(this._walletNewVM, this._seedTypeViewModel); @@ -74,6 +75,7 @@ class _WalletNameFormState extends State { _walletNewVM.hasWalletPassword ? TextEditingController() : null; static const aspectRatioImage = 1.22; + static bool formProcessing = false; final GlobalKey _formKey; final GlobalKey _languageSelectorKey; @@ -347,26 +349,35 @@ class _WalletNameFormState extends State { ); } - void _confirmForm() { - if (_formKey.currentState != null && !_formKey.currentState!.validate()) { - return; - } - if (_walletNewVM.nameExists(_walletNewVM.name)) { - showPopUp( - context: context, - builder: (_) { - return AlertWithOneAction( - alertTitle: '', - alertContent: S.of(context).wallet_name_exists, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } else { - _walletNewVM.create( - options: _walletNewVM.hasLanguageSelector - ? [_languageSelectorKey.currentState!.selected, isPolyseed] - : null); + void _confirmForm() async { + if (formProcessing) return; + formProcessing = true; + try { + if (_formKey.currentState != null && !_formKey.currentState!.validate()) { + formProcessing = false; + return; + } + if (_walletNewVM.nameExists(_walletNewVM.name)) { + await showPopUp( + context: context, + builder: (_) { + return AlertWithOneAction( + alertTitle: '', + alertContent: S.of(context).wallet_name_exists, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } else { + await _walletNewVM.create( + options: _walletNewVM.hasLanguageSelector + ? [_languageSelectorKey.currentState!.selected, isPolyseed] + : null); + } + } catch (e) { + formProcessing = false; + rethrow; } + formProcessing = false; } bool get isPolyseed => widget._seedTypeViewModel.moneroSeedType == SeedType.polyseed; diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index cd6383c0d..29bc29986 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/new_wallet_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'; @@ -80,6 +81,8 @@ class WalletRestorePage extends BasePage { }); } + static bool formProcessing = false; + @override Widget middle(BuildContext context) => Observer( builder: (_) => Text( @@ -350,75 +353,85 @@ class WalletRestorePage extends BasePage { } Future _confirmForm(BuildContext context) async { - // Dismissing all visible keyboard to provide context for navigation - FocusManager.instance.primaryFocus?.unfocus(); + if (formProcessing) return; + formProcessing = true; + try { + // Dismissing all visible keyboard to provide context for navigation + FocusManager.instance.primaryFocus?.unfocus(); - late BuildContext? formContext; - late GlobalKey? formKey; - late String name; - if (walletRestoreViewModel.mode == WalletRestoreMode.seed) { - formContext = walletRestoreFromSeedFormKey.currentContext; - formKey = walletRestoreFromSeedFormKey.currentState!.formKey; - name = walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.value.text; - } else if (walletRestoreViewModel.mode == WalletRestoreMode.keys) { - formContext = walletRestoreFromKeysFormKey.currentContext; - formKey = walletRestoreFromKeysFormKey.currentState!.formKey; - name = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.value.text; - } - - if (!formKey!.currentState!.validate()) { - return; - } - - if (walletRestoreViewModel.nameExists(name)) { - showNameExistsAlert(formContext!); - return; - } - - walletRestoreViewModel.state = IsExecutingState(); - - DerivationInfo? dInfo; - - // get info about the different derivations: - List derivations = - await walletRestoreViewModel.getDerivationInfo(_credentials()); - - int derivationsWithHistory = 0; - int derivationWithHistoryIndex = 0; - for (int i = 0; i < derivations.length; i++) { - if (derivations[i].transactionsCount > 0) { - derivationsWithHistory++; - derivationWithHistoryIndex = i; + late BuildContext? formContext; + late GlobalKey? formKey; + late String name; + if (walletRestoreViewModel.mode == WalletRestoreMode.seed) { + formContext = walletRestoreFromSeedFormKey.currentContext; + formKey = walletRestoreFromSeedFormKey.currentState!.formKey; + name = walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.value.text; + } else if (walletRestoreViewModel.mode == WalletRestoreMode.keys) { + formContext = walletRestoreFromKeysFormKey.currentContext; + formKey = walletRestoreFromKeysFormKey.currentState!.formKey; + name = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.value.text; } - } - if (derivationsWithHistory > 1) { - dInfo = await Navigator.of(context).pushNamed( - Routes.restoreWalletChooseDerivation, - arguments: derivations, - ) as DerivationInfo?; - } else if (derivationsWithHistory == 1) { - dInfo = derivations[derivationWithHistoryIndex]; - } - - // get the default derivation for this wallet type: - if (dInfo == null) { - // we only return 1 derivation if we're pretty sure we know which one to use: - if (derivations.length == 1) { - dInfo = derivations.first; - } else { - // if we have multiple possible derivations, and none have histories - // we just default to the most common one: - dInfo = walletRestoreViewModel.getCommonRestoreDerivation(); + if (!formKey!.currentState!.validate()) { + formProcessing = false; + return; } - } - this.derivationInfo = dInfo; - if (this.derivationInfo == null) { - this.derivationInfo = walletRestoreViewModel.getDefaultDerivation(); - } + if (walletRestoreViewModel.nameExists(name)) { + showNameExistsAlert(formContext!); + formProcessing = false; + return; + } - walletRestoreViewModel.create(options: _credentials()); + walletRestoreViewModel.state = IsExecutingState(); + + DerivationInfo? dInfo; + + // get info about the different derivations: + List derivations = + await walletRestoreViewModel.getDerivationInfo(_credentials()); + + int derivationsWithHistory = 0; + int derivationWithHistoryIndex = 0; + for (int i = 0; i < derivations.length; i++) { + if (derivations[i].transactionsCount > 0) { + derivationsWithHistory++; + derivationWithHistoryIndex = i; + } + } + + if (derivationsWithHistory > 1) { + dInfo = await Navigator.of(context).pushNamed( + Routes.restoreWalletChooseDerivation, + arguments: derivations, + ) as DerivationInfo?; + } else if (derivationsWithHistory == 1) { + dInfo = derivations[derivationWithHistoryIndex]; + } + + // get the default derivation for this wallet type: + if (dInfo == null) { + // we only return 1 derivation if we're pretty sure we know which one to use: + if (derivations.length == 1) { + dInfo = derivations.first; + } else { + // if we have multiple possible derivations, and none have histories + // we just default to the most common one: + dInfo = walletRestoreViewModel.getCommonRestoreDerivation(); + } + } + + this.derivationInfo = dInfo; + if (this.derivationInfo == null) { + this.derivationInfo = walletRestoreViewModel.getDefaultDerivation(); + } + + await walletRestoreViewModel.create(options: _credentials()); + } catch (e) { + formProcessing = false; + rethrow; + } + formProcessing = false; } Future showNameExistsAlert(BuildContext context) { From 4e7e00b975816cffec2d23866c3b5d59fb8445b9 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 13 Aug 2024 19:25:18 +0100 Subject: [PATCH 026/142] Fix: Remove resolution scope from unstoppable domains plugin (#1604) --- ios/Runner/AppDelegate.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 0cc4eebe8..402f6556b 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -3,11 +3,7 @@ import Flutter import workmanager @UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - lazy var resolution : Resolution? = { - return try? Resolution() - }() - +@objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? From 525df820c3f6bccf7d2f1b419ae261d015bee37f Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 13 Aug 2024 22:51:25 +0300 Subject: [PATCH 027/142] Generic fixes (#1605) * minor fixes * fix not saving wallet password to secure storage * cache linux deps as well --- .github/workflows/cache_dependencies.yml | 5 ++++- cw_monero/lib/api/wallet_manager.dart | 3 --- lib/core/wallet_creation_service.dart | 13 ++++++------- lib/src/screens/new_wallet/new_wallet_page.dart | 12 ++++++------ lib/src/screens/restore/wallet_restore_page.dart | 15 +++++++-------- lib/view_model/wallet_creation_vm.dart | 2 -- 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index cca5bb4bf..72a74a8b2 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -60,7 +60,7 @@ jobs: path: | /opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/scripts/monero_c/release - key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh') }} + key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals @@ -68,3 +68,6 @@ jobs: cd /opt/android/cake_wallet/scripts/android/ source ./app_env.sh cakewallet ./build_monero_all.sh + cd ../linux/ + source ./app_env.sh cakewallet + ./build_monero_all.sh diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 14bf92d16..ce4d41010 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -8,10 +8,7 @@ import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; import 'package:cw_monero/api/wallet.dart'; -import 'package:flutter/foundation.dart'; import 'package:cw_monero/api/transaction_history.dart'; -import 'package:cw_monero/api/wallet.dart'; -import 'package:flutter/foundation.dart'; import 'package:monero/monero.dart' as monero; class MoneroCException implements Exception { diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 823aa7e84..1e9299282 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/wallet_info.dart'; @@ -57,9 +56,9 @@ class WalletCreationService { if (credentials.password == null) { credentials.password = generateWalletPassword(); - await keyService.saveWalletPassword( - password: credentials.password!, walletName: credentials.name); } + await keyService.saveWalletPassword( + password: credentials.password!, walletName: credentials.name); if (_hasSeedPhraseLengthOption) { credentials.seedPhraseLength = settingsStore.seedPhraseLength.value; @@ -99,9 +98,9 @@ class WalletCreationService { if (credentials.password == null) { credentials.password = generateWalletPassword(); - await keyService.saveWalletPassword( - password: credentials.password!, walletName: credentials.name); } + await keyService.saveWalletPassword( + password: credentials.password!, walletName: credentials.name); final wallet = await _service!.restoreFromKeys(credentials, isTestnet: isTestnet); @@ -118,9 +117,9 @@ class WalletCreationService { if (credentials.password == null) { credentials.password = generateWalletPassword(); - await keyService.saveWalletPassword( - password: credentials.password!, walletName: credentials.name); } + await keyService.saveWalletPassword( + password: credentials.password!, walletName: credentials.name); final wallet = await _service!.restoreFromSeed(credentials, isTestnet: isTestnet); diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index cd5a7ce8d..cb451c056 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -75,7 +75,7 @@ class _WalletNameFormState extends State { _walletNewVM.hasWalletPassword ? TextEditingController() : null; static const aspectRatioImage = 1.22; - static bool formProcessing = false; + bool _formProcessing = false; final GlobalKey _formKey; final GlobalKey _languageSelectorKey; @@ -350,11 +350,11 @@ class _WalletNameFormState extends State { } void _confirmForm() async { - if (formProcessing) return; - formProcessing = true; + if (_formProcessing) return; + _formProcessing = true; try { if (_formKey.currentState != null && !_formKey.currentState!.validate()) { - formProcessing = false; + _formProcessing = false; return; } if (_walletNewVM.nameExists(_walletNewVM.name)) { @@ -374,10 +374,10 @@ class _WalletNameFormState extends State { : null); } } catch (e) { - formProcessing = false; + _formProcessing = false; rethrow; } - formProcessing = false; + _formProcessing = false; } bool get isPolyseed => widget._seedTypeViewModel.moneroSeedType == SeedType.polyseed; diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 29bc29986..c8fa3665e 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -2,7 +2,6 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/new_wallet/new_wallet_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'; @@ -81,7 +80,7 @@ class WalletRestorePage extends BasePage { }); } - static bool formProcessing = false; + bool _formProcessing = false; @override Widget middle(BuildContext context) => Observer( @@ -353,8 +352,8 @@ class WalletRestorePage extends BasePage { } Future _confirmForm(BuildContext context) async { - if (formProcessing) return; - formProcessing = true; + if (_formProcessing) return; + _formProcessing = true; try { // Dismissing all visible keyboard to provide context for navigation FocusManager.instance.primaryFocus?.unfocus(); @@ -373,13 +372,13 @@ class WalletRestorePage extends BasePage { } if (!formKey!.currentState!.validate()) { - formProcessing = false; + _formProcessing = false; return; } if (walletRestoreViewModel.nameExists(name)) { showNameExistsAlert(formContext!); - formProcessing = false; + _formProcessing = false; return; } @@ -428,10 +427,10 @@ class WalletRestorePage extends BasePage { await walletRestoreViewModel.create(options: _credentials()); } catch (e) { - formProcessing = false; + _formProcessing = false; rethrow; } - formProcessing = false; + _formProcessing = false; } Future showNameExistsAlert(BuildContext context) { diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 43386494e..5a9a1d093 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -110,8 +110,6 @@ abstract class WalletCreationVMBase with Store { _appStore.authenticationStore.allowed(); state = ExecutedSuccessfullyState(); } catch (e, s) { - print("@@@@@@@@"); - print(s); state = FailureState(e.toString()); } } From b0ece46f29b3829482f42349c8688d74caa43b7d Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Tue, 13 Aug 2024 23:15:32 +0300 Subject: [PATCH 028/142] use the cached deps for both jobs [skip ci] --- .github/workflows/pr_test_build_android.yml | 2 +- .github/workflows/pr_test_build_linux.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index ea8770860..5dbf1610f 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -78,7 +78,7 @@ jobs: path: | /opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/scripts/monero_c/release - key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh') }} + key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 12c930120..37253e96f 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -75,7 +75,7 @@ jobs: path: | /opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/scripts/monero_c/release - key: linux-${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh') }} + key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals From 390fdda0231d45d6aff44b5d837e19afc04d3068 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Wed, 14 Aug 2024 00:58:13 +0300 Subject: [PATCH 029/142] fix caching linux deps --- .github/workflows/cache_dependencies.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index 72a74a8b2..f5fbf4826 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -37,7 +37,12 @@ jobs: channel: stable - name: Install package dependencies - run: sudo apt-get install -y curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang + run: sudo apt-get install -y curl unzip automake build-essential file pkg-config git python-is-python3 libtool libtinfo5 cmake clang + + - name: Install desktop dependencies + run: | + sudo apt update + sudo apt install -y ninja-build libgtk-3-dev gperf - name: Execute Build and Setup Commands run: | @@ -70,4 +75,5 @@ jobs: ./build_monero_all.sh cd ../linux/ source ./app_env.sh cakewallet + ./app_config.sh ./build_monero_all.sh From 20252cdea8b4c822ef643dd2e3cc2315b977aed1 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Wed, 14 Aug 2024 01:37:04 +0300 Subject: [PATCH 030/142] cache only android deps only have linux build as an artifact --- .github/workflows/cache_dependencies.yml | 11 +---------- .github/workflows/pr_test_build_linux.yml | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index f5fbf4826..902a44a42 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -37,12 +37,7 @@ jobs: channel: stable - name: Install package dependencies - run: sudo apt-get install -y curl unzip automake build-essential file pkg-config git python-is-python3 libtool libtinfo5 cmake clang - - - name: Install desktop dependencies - run: | - sudo apt update - sudo apt install -y ninja-build libgtk-3-dev gperf + run: sudo apt-get install -y curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang - name: Execute Build and Setup Commands run: | @@ -73,7 +68,3 @@ jobs: cd /opt/android/cake_wallet/scripts/android/ source ./app_env.sh cakewallet ./build_monero_all.sh - cd ../linux/ - source ./app_env.sh cakewallet - ./app_config.sh - ./build_monero_all.sh diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 37253e96f..c1a3a3be4 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -174,13 +174,14 @@ jobs: with: path: /opt/android/cake_wallet/build/linux/x64/release/${{env.BRANCH_NAME}}.zip - - name: Send Test APK - continue-on-error: true - uses: adrey/slack-file-upload-action@1.0.5 - with: - token: ${{ secrets.SLACK_APP_TOKEN }} - path: /opt/android/cake_wallet/build/linux/x64/release/${{env.BRANCH_NAME}}.zip - channel: ${{ secrets.SLACK_APK_CHANNEL }} - title: "${{ env.BRANCH_NAME }}_linux.zip" - filename: ${{ env.BRANCH_NAME }}_linux.zip - initial_comment: ${{ github.event.head_commit.message }} +# Just as an artifact would be enough +# - name: Send Test APK +# continue-on-error: true +# uses: adrey/slack-file-upload-action@1.0.5 +# with: +# token: ${{ secrets.SLACK_APP_TOKEN }} +# path: /opt/android/cake_wallet/build/linux/x64/release/${{env.BRANCH_NAME}}.zip +# channel: ${{ secrets.SLACK_APK_CHANNEL }} +# title: "${{ env.BRANCH_NAME }}_linux.zip" +# filename: ${{ env.BRANCH_NAME }}_linux.zip +# initial_comment: ${{ github.event.head_commit.message }} From 183f9c191d292ed9fb5d6b1d1bcf6982e1424445 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 14 Aug 2024 17:49:56 +0300 Subject: [PATCH 031/142] Update pr_test_build_linux.yml --- .github/workflows/pr_test_build_linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index c1a3a3be4..7935dd177 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -75,7 +75,7 @@ jobs: path: | /opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/scripts/monero_c/release - key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} + key: linux_${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals From d96bab43c9fee91b464166f7747be3b864b7f5ff Mon Sep 17 00:00:00 2001 From: Rafael Date: Wed, 14 Aug 2024 20:40:40 -0300 Subject: [PATCH 032/142] fix: p2sh addr (#1607) --- cw_bitcoin/lib/utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart index 29d7a9bf3..a7435bed1 100644 --- a/cw_bitcoin/lib/utils.dart +++ b/cw_bitcoin/lib/utils.dart @@ -23,7 +23,7 @@ String generateP2SHAddress({ required int index, }) => ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) - .toP2wshInP2sh() + .toP2wpkhInP2sh() .toAddress(network); String generateP2WSHAddress({ From 4fa487fa275b9d1fc1343c7852d763b899dcb74b Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 15 Aug 2024 01:41:22 +0200 Subject: [PATCH 033/142] seed fixes (#1594) * seed fixes + jCenter removal * set seed language to English if none show error when requesting seed --------- Co-authored-by: Omar Hatem --- cw_monero/lib/api/wallet.dart | 5 ++++- cw_monero/lib/api/wallet_manager.dart | 11 ++++++++++- cw_wownero/lib/api/wallet.dart | 5 ++++- cw_wownero/lib/api/wallet_manager.dart | 11 ++++++++++- lib/view_model/dashboard/dashboard_view_model.dart | 13 ++++++++----- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 0f6e59c4e..973a38535 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -39,7 +39,7 @@ String getSeed() { if (polyseed != "") { return polyseed; } - final legacy = monero.Wallet_seed(wptr!, seedOffset: ''); + final legacy = getSeedLegacy("English"); return legacy; } @@ -49,6 +49,9 @@ String getSeedLegacy(String? language) { monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); legacy = monero.Wallet_seed(wptr!, seedOffset: ''); } + if (monero.Wallet_status(wptr!) != 0) { + return monero.Wallet_errorString(wptr!); + } return legacy; } diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index ce4d41010..f06fe3e66 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -123,7 +123,16 @@ void restoreWalletFromKeysSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - final newWptr = monero.WalletManager_createWalletFromKeys( + final newWptr = spendKey != "" + ? monero.WalletManager_createDeterministicWalletFromSpendKey( + wmPtr, + path: path, + password: password, + language: language, + spendKeyString: spendKey, + newWallet: true, // TODO(mrcyjanek): safe to remove + restoreHeight: restoreHeight) + : monero.WalletManager_createWalletFromKeys( wmPtr, path: path, password: password, diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 96822dfe4..2ccd560ed 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -41,7 +41,7 @@ String getSeed() { if (polyseed != "") { return polyseed; } - final legacy = wownero.Wallet_seed(wptr!, seedOffset: ''); + final legacy = getSeedLegacy(null); return legacy; } @@ -51,6 +51,9 @@ String getSeedLegacy(String? language) { wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); legacy = wownero.Wallet_seed(wptr!, seedOffset: ''); } + if (wownero.Wallet_status(wptr!) != 0) { + return wownero.Wallet_errorString(wptr!); + } return legacy; } diff --git a/cw_wownero/lib/api/wallet_manager.dart b/cw_wownero/lib/api/wallet_manager.dart index 7915373bb..660433ba6 100644 --- a/cw_wownero/lib/api/wallet_manager.dart +++ b/cw_wownero/lib/api/wallet_manager.dart @@ -140,7 +140,16 @@ void restoreWalletFromKeysSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - final newWptr = wownero.WalletManager_createWalletFromKeys( + final newWptr = spendKey != "" + ? wownero.WalletManager_createDeterministicWalletFromSpendKey( + wmPtr, + path: path, + password: password, + language: language, + spendKeyString: spendKey, + newWallet: true, // TODO(mrcyjanek): safe to remove + restoreHeight: restoreHeight) + : wownero.WalletManager_createWalletFromKeys( wmPtr, path: path, password: password, diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 1baea76cd..e98412dce 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -362,12 +362,15 @@ abstract class DashboardViewModelBase with Store { if (wallet.type != WalletType.monero) return []; final keys = monero!.getKeys(wallet); List errors = [ - if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0", + // leaving these commented out for now, I'll be able to fix that properly in the airgap update + // to not cause work duplication, this will do the job as well, it will be slightly less precise + // about what happened - but still enough. + // if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0", if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("")) "private view key is 0", - if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0", - if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) "private view key is 0", - if (wallet.seed == null) "wallet seed is null", - if (wallet.seed == "") "wallet seed is empty", + // if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0", + if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) "public view key is 0", + // if (wallet.seed == null) "wallet seed is null", + // if (wallet.seed == "") "wallet seed is empty", if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address == "41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4") "primary address is invalid, you won't be able to receive / spend funds", ]; From 40496fbfe73d56903213c40482a6ab6ee03a5022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Brunner?= Date: Thu, 15 Aug 2024 19:04:11 +0200 Subject: [PATCH 034/142] Major overhaul of the Tagalog localization (#1611) --- res/values/strings_tl.arb | 1058 ++++++++++++++++++------------------- 1 file changed, 529 insertions(+), 529 deletions(-) diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 3bbae2e50..22093f772 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -1,735 +1,735 @@ { - "about_cake_pay": "Pinapayagan ka ng cake Pay na madaling bumili ng mga kard ng regalo na may mga virtual na pag -aari, gastusin agad sa higit sa 150,000 mga mangangalakal sa Estados Unidos.", + "about_cake_pay": "Binibigyan-daan ka ng Cake Pay na madaling makabili ng mga gift card na may mga virtual na asset na magagastos kaagad sa mahigit na 150,000 merchant sa United States.", "account": "Account", "accounts": "Mga Account", - "accounts_subaddresses": "Mga Account at Subaddresses", - "activate": "Buhayin", + "accounts_subaddresses": "Mga account at mga subaddress", + "activate": "Aktibahin", "active": "Aktibo", - "active_cards": "Mga aktibong kard", + "active_cards": "Mga aktibong card", "activeConnectionsPrompt": "Lalabas dito ang mga aktibong koneksyon", - "add": "Idagdag", + "add": "Magdagdag", "add_contact": "Magdagdag ng contact", - "add_contact_to_address_book": "Nais mo bang idagdag ang contact na ito sa iyong address book?", - "add_custom_node": "Magdagdag ng bagong pasadyang node", - "add_custom_redemption": "Magdagdag ng pasadyang pagtubos", - "add_fund_to_card": "Magdagdag ng prepaid na pondo sa mga kard (hanggang sa ${value})", + "add_contact_to_address_book": "Gusto mo bang idagdag ang contact na ito sa iyong address book?", + "add_custom_node": "Magdagdag ng Bagong Custom Node", + "add_custom_redemption": "Magdagdag ng Custom Redemption", + "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_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", - "add_token_disclaimer_check": "Kinumpirma ko ang address ng kontrata ng token at impormasyon gamit ang isang kagalang -galang na mapagkukunan. Ang pagdaragdag ng nakakahamak o hindi tamang impormasyon ay maaaring magresulta sa pagkawala ng mga pondo.", - "add_token_warning": "Huwag i -edit o magdagdag ng mga token tulad ng itinuro ng mga scammers.\nLaging kumpirmahin ang mga token address na may mga kagalang -galang na mapagkukunan!", + "add_tip": "Magdagdag ng Tip", + "add_token_disclaimer_check": "Kinumpirma ko ang address ng kontrata ng token at impormasyon gamit ang isang kagalang-galang na mapagkukunan. Ang pagdaragdag ng nakakahamak o hindi tamang impormasyon ay maaaring magresulta sa pagkawala ng mga pondo.", + "add_token_warning": "Huwag i-edit o magdagdag ng mga token tulad ng itinuro ng mga scammers.\nLaging kumpirmahin ang mga token address na may mga kagalang-galang na mapagkukunan!", "add_value": "Magdagdag ng halaga", "address": "Address", "address_book": "Address Book", - "address_book_menu": "Address Book", + "address_book_menu": "Address book", "address_detected": "Nakita ang address", - "address_from_domain": "Ang address na ito ay mula sa ${domain} sa mga hindi mapigilan na mga domain", - "address_from_yat": "Ang address na ito ay mula sa ${emoji} sa yat", - "address_label": "Address Label", + "address_from_domain": "Ang address na ito ay mula sa ${domain} mga Unstoppable Domains", + "address_from_yat": "Ang address na ito ay mula sa ${emoji} sa Yat", + "address_label": "Label ng address", "address_remove_contact": "Alisin ang contact", "address_remove_content": "Sigurado ka bang nais mong alisin ang napiling contact?", - "addresses": "Mga address", + "addresses": "Mga Address", "advanced_settings": "Mga Advanced na Setting", "aggressive": "Agresibo", - "agree": "Sumang -ayon", - "agree_and_continue": "Sumang -ayon at magpatuloy", - "agree_to": "Sa pamamagitan ng paglikha ng account sumasang -ayon ka sa", - "all": "Lahat", - "all_trades": "Lahat ng mga kalakal", + "agree": "Sumang-ayon", + "agree_and_continue": "Sumang-ayon & Magpatuloy", + "agree_to": "Sa pamamagitan ng paggawa ng account sumasang-ayon ka sa ", + "all": "LAHAT", + "all_trades": "Lahat ng mga trade", "all_transactions": "Lahat ng mga transaksyon", "alphabetical": "Alpabeto", "already_have_account": "Mayroon nang account?", "always": "Palagi", - "amount": "Halaga:", + "amount": "Halaga: ", "amount_is_below_minimum_limit": "Ang iyong balanse pagkatapos ng mga bayarin ay mas mababa kaysa sa minimum na halaga na kinakailangan para sa palitan (${min})", "amount_is_estimate": "Ang natanggap na halaga ay isang pagtatantya", "amount_is_guaranteed": "Ang natanggap na halaga ay garantisado", "and": "at", - "anonpay_description": "Bumuo ng ${type}. Ang tatanggap ay maaaring ${method} na may anumang suportadong cryptocurrency, at makakatanggap ka ng mga pondo sa pitaka na ito.", - "apk_update": "APK Update", + "anonpay_description": "Bumuo ng ${type}. Ang tatanggap ay maaaring ${method} na may anumang suportadong cryptocurrency, at makakatanggap ka ng mga pondo sa wallet na ito.", + "apk_update": "APK update", "approve": "Aprubahan", "arrive_in_this_address": "Ang ${currency} ${tag} ay darating sa address na ito", "ascending": "Umakyat", - "ask_each_time": "Magtanong sa bawat oras", - "auth_store_ban_timeout": "ban_timeout", - "auth_store_banned_for": "Pinagbawalan para sa", - "auth_store_banned_minutes": "minuto", - "auth_store_incorrect_password": "Maling pin", + "ask_each_time": "Magtanong sa tuwing", + "auth_store_ban_timeout": "ban timeout", + "auth_store_banned_for": "Pinagbawalan para sa ", + "auth_store_banned_minutes": " minuto", + "auth_store_incorrect_password": "Maling PIN", "authenticated": "Napatunayan", "authentication": "Pagpapatunay", "auto_generate_addresses": "Auto bumuo ng mga address", - "auto_generate_subaddresses": "Ang Auto ay bumubuo ng mga subaddresses", + "auto_generate_subaddresses": "Auto bumuo ng mga subaddress", "automatic": "Awtomatiko", - "available_balance": "Magagamit na balanse", + "available_balance": "Magagamit na Balanse", "available_balance_description": "Ang \"magagamit na balanse\" o \"nakumpirma na balanse\" ay mga pondo na maaaring gastusin kaagad. Kung lumilitaw ang mga pondo sa mas mababang balanse ngunit hindi ang nangungunang balanse, dapat kang maghintay ng ilang minuto para sa mga papasok na pondo upang makakuha ng mas maraming mga kumpirmasyon sa network. Matapos silang makakuha ng higit pang mga kumpirmasyon, gugugol sila.", "avg_savings": "Avg. Matitipid", "awaitDAppProcessing": "Pakihintay na matapos ang pagproseso ng dApp.", - "awaiting_payment_confirmation": "Naghihintay ng kumpirmasyon sa pagbabayad", - "background_sync_mode": "Mode ng pag -sync ng background", + "awaiting_payment_confirmation": "Nanghihintay ng Kumpirmasyon sa Pagbabayad", + "background_sync_mode": "Background sync mode", "backup": "Backup", - "backup_file": "Backup file", - "backup_password": "Backup password", - "balance": "Balansehin", + "backup_file": "Backup na file", + "backup_password": "Backup na password", + "balance": "Balanse", "balance_page": "Pahina ng Balanse", "bill_amount": "Halaga ng Bill", - "billing_address_info": "Kung tatanungin ang isang address ng pagsingil, ibigay ang iyong address sa pagpapadala", - "biometric_auth_reason": "I -scan ang iyong fingerprint upang mapatunayan", - "bitcoin_dark_theme": "Bitcoin Madilim na Tema", - "bitcoin_light_theme": "Tema ng ilaw ng bitcoin", - "bitcoin_payments_require_1_confirmation": "Ang mga pagbabayad sa Bitcoin ay nangangailangan ng 1 kumpirmasyon, na maaaring tumagal ng 20 minuto o mas mahaba. Salamat sa iyong pasensya! Mag -email ka kapag nakumpirma ang pagbabayad.", - "block_remaining": "1 bloke ang natitira", + "billing_address_info": "Kung humihingi ng billing address, ibigay ang iyong shipping address", + "biometric_auth_reason": "I-scan ang iyong fingerprint para ma-authenticate", + "bitcoin_dark_theme": "Bitcoin Dark Theme", + "bitcoin_light_theme": "Bitcoin Light Theme", + "bitcoin_payments_require_1_confirmation": "Ang mga pagbabayad sa Bitcoin ay nangangailangan ng 1 kumpirmasyon, na maaaring tumagal ng 20 minuto o mas mahaba. Salamat sa iyong pasensya! Mag-email ka kapag nakumpirma ang pagbabayad.", + "block_remaining": "1 Bloke ang Natitira", "Blocks_remaining": "Ang natitirang ${status} ay natitira", "bluetooth": "Bluetooth", - "bright_theme": "Maliwanag", - "bump_fee": "Bayad sa paga", - "buy": "Bilhin", + "bright_theme": "Bright", + "bump_fee": "Dagdagan ang fee", + "buy": "Bumili", "buy_alert_content": "Sa kasalukuyan ay sinusuportahan lamang namin ang pagbili ng Bitcoin, Ethereum, Litecoin, at Monero. Mangyaring lumikha o lumipat sa iyong Bitcoin, Ethereum, Litecoin, o Monero Wallet.", - "buy_bitcoin": "Bumili ng bitcoin", - "buy_now": "Bumili ka na ngayon", + "buy_bitcoin": "Bumili ng Bitcoin", + "buy_now": "Bumili Ngayon", "buy_provider_unavailable": "Kasalukuyang hindi available ang provider.", - "buy_with": "Bumili ka", - "by_cake_pay": "sa pamamagitan ng cake pay", - "cake_2fa_preset": "Cake 2fa preset", - "cake_dark_theme": "Cake madilim na tema", - "cake_pay_account_note": "Mag -sign up na may isang email address lamang upang makita at bumili ng mga kard. Ang ilan ay magagamit kahit sa isang diskwento!", - "cake_pay_learn_more": "Agad na bumili at tubusin ang mga kard ng regalo sa app!\nMag -swipe pakaliwa sa kanan upang matuto nang higit pa.", - "cake_pay_save_order": "Ang card ay dapat ipadala sa iyong e-mail sa loob ng 1 araw ng negosyo \n i-save ang iyong order ID:", + "buy_with": "Bumili ng", + "by_cake_pay": "by Cake Pay", + "cake_2fa_preset": "Cake 2FA Preset", + "cake_dark_theme": "Cake Dark Theme", + "cake_pay_account_note": "Mag-sign up na may isang email address lamang upang makita at bumili ng mga kard. Ang ilan ay magagamit kahit sa isang diskwento!", + "cake_pay_learn_more": "Agad na bumili at tubusin ang mga kard ng regalo sa app!\nMag-swipe pakaliwa sa kanan upang matuto nang higit pa.", + "cake_pay_save_order": "Ang card ay dapat ipadala sa iyong email sa loob ng 1 araw ng negosyo \n I-save ang iyong order ID:", "cake_pay_subtitle": "Bumili ng mga pandaigdigang prepaid card at gift card", "cake_pay_web_cards_subtitle": "Bumili ng mga pandaigdigang prepaid card at gift card", - "cake_pay_web_cards_title": "Cake pay web card", - "cake_wallet": "Cake wallet", - "cakepay_prepaid_card": "Cakepay prepaid debit card", + "cake_pay_web_cards_title": "Cake Pay Web Cards", + "cake_wallet": "Cake Wallet", + "cakepay_prepaid_card": "CakePay Prepaid Debit Card", "camera_consent": "Gagamitin ang iyong camera upang kumuha ng larawan para sa mga layunin ng pagkakakilanlan sa pamamagitan ng ${provider}. Pakisuri ang kanilang Patakaran sa Privacy para sa mga detalye.", "camera_permission_is_required": "Kinakailangan ang pahintulot sa camera.\nMangyaring paganahin ito mula sa mga setting ng app.", "cancel": "Kanselahin", "card_address": "Address:", "cardholder_agreement": "Kasunduan sa Cardholder", "cards": "Mga Card", - "chains": "Mga tanikala", - "change": "Palitan", - "change_backup_password_alert": "Ang iyong mga nakaraang backup file ay hindi magagamit upang mai -import gamit ang bagong backup password. Ang bagong backup na password ay gagamitin lamang para sa mga bagong backup file. Sigurado ka bang nais mong baguhin ang backup password?", + "chains": "Mga Chain", + "change": "Sukli", + "change_backup_password_alert": "Ang iyong mga nakaraang backup na file ay hindi magagamit upang i-import gamit ang bagong backup na password. Ang bagong backup na password ay gagamitin lamang para sa mga bagong backup na file. Sigurado ka bang gusto mong baguhin ang backup na password?", "change_currency": "Baguhin ang pera", "change_current_node": "Sigurado ka bang baguhin ang kasalukuyang node sa ${node}?", "change_current_node_title": "Baguhin ang kasalukuyang node", - "change_exchange_provider": "Baguhin ang tagapagbigay ng palitan", + "change_exchange_provider": "Baguhin ang exchange provider", "change_language": "Baguhin ang wika", "change_language_to": "Baguhin ang wika sa ${language}?", - "change_password": "Palitan ANG password", - "change_rep": "Baguhin ang kinatawan", - "change_rep_message": "Sigurado ka bang nais mong baguhin ang mga kinatawan?", - "change_rep_successful": "Matagumpay na nagbago ng kinatawan", - "change_wallet_alert_content": "Nais mo bang baguhin ang kasalukuyang pitaka sa ${wallet_name}?", - "change_wallet_alert_title": "Baguhin ang kasalukuyang pitaka", + "change_password": "Baguhin ang password", + "change_rep": "Baguhin ang Representative", + "change_rep_message": "Sigurado ka bang nais mong baguhin ang mga representative?", + "change_rep_successful": "Matagumpay na nagbago ng representative", + "change_wallet_alert_content": "Gusto mo bang palitan ang kasalukuyang wallet sa ${wallet_name}?", + "change_wallet_alert_title": "Baguhin ang kasalukuyang wallet", "choose_account": "Pumili ng account", "choose_address": "Mangyaring piliin ang address:", "choose_card_value": "Pumili ng isang halaga ng card", - "choose_derivation": "Piliin ang derivation ng Wallet", + "choose_derivation": "Piliin ang Derivation ng Wallet", "choose_from_available_options": "Pumili mula sa magagamit na mga pagpipilian:", "choose_one": "Pumili ng isa", "choose_relay": "Mangyaring pumili ng relay na gagamitin", - "choose_wallet_currency": "Mangyaring piliin ang Pera ng Wallet:", - "clear": "Malinaw", + "choose_wallet_currency": "Mangyaring piliin ang pera ng wallet:", + "clear": "Burahin", "clearnet_link": "Link ng Clearnet", - "close": "Malapit", - "coin_control": "Control ng barya (opsyonal)", - "cold_or_recover_wallet": "Magdagdag ng isang malamig na pitaka o mabawi ang isang wallet ng papel", - "color_theme": "Tema ng kulay", - "commit_transaction_amount_fee": "Gumawa ng transaksyon\nHalaga: ${amount}\nBayad: ${fee}", + "close": "Isara", + "coin_control": "Coin control (opsyonal)", + "cold_or_recover_wallet": "Magdagdag ng isang cold wallet o mabawi ang isang paper wallet", + "color_theme": "Color theme", + "commit_transaction_amount_fee": "Gumawa ng transaksyon\nHalaga: ${amount}\nFee: ${fee}", "confirm": "Kumpirmahin", - "confirm_delete_template": "Ang pagkilos na ito ay tatanggalin ang template na ito. Nais mo bang magpatuloy?", - "confirm_delete_wallet": "Ang pagkilos na ito ay tatanggalin ang pitaka na ito. Nais mo bang magpatuloy?", - "confirm_fee_deduction": "Kumpirmahin ang pagbabawas ng bayad", - "confirm_fee_deduction_content": "Sumasang -ayon ka bang bawasan ang bayad mula sa output?", + "confirm_delete_template": "Tatanggalin ng pagkilos na ito ang template na ito. Gusto mo bang magpatuloy?", + "confirm_delete_wallet": "Tatanggalin ng pagkilos na ito ang wallet na ito. Gusto mo bang magpatuloy?", + "confirm_fee_deduction": "Kumpirmahin ang pagbabawas ng fee", + "confirm_fee_deduction_content": "Sumasang-ayon ka bang bawasan ang fee mula sa output?", "confirm_sending": "Kumpirmahin ang pagpapadala", - "confirm_silent_payments_switch_node": "Ang iyong kasalukuyang node ay hindi sumusuporta sa tahimik na pagbabayad \\ ncake wallet ay lilipat sa isang katugmang node, para lamang sa pag -scan", + "confirm_silent_payments_switch_node": "Ang iyong kasalukuyang node ay hindi sumusuporta sa tahimik na pagbabayad \\ nCake Wallet ay lilipat sa isang katugmang node, para lamang sa pag-scan", "confirmations": "Mga kumpirmasyon", - "confirmed": "Nakumpirma na balanse", + "confirmed": "Nakumpirma na Balanse", "confirmed_tx": "Nakumpirma", - "congratulations": "Binabati kita!", - "connect_an_existing_yat": "Ikonekta ang isang umiiral na yat", - "connect_yats": "Ikonekta ang mga yats", - "connect_your_hardware_wallet": "Ikonekta ang iyong wallet ng hardware gamit ang Bluetooth o USB", - "connect_your_hardware_wallet_ios": "Ikonekta ang iyong wallet ng hardware gamit ang Bluetooth", - "connection_sync": "Koneksyon at pag -sync", + "congratulations": "Congratulations!", + "connect_an_existing_yat": "Ikonekta ang isang umiiral na Yat", + "connect_yats": "Ikonekta sa Yats", + "connect_your_hardware_wallet": "Ikonekta ang iyong hardware wallet gamit ang Bluetooth o USB", + "connect_your_hardware_wallet_ios": "Ikonekta ang iyong wallet gamit ang Bluetooth", + "connection_sync": "Koneksyon at pag-sync", "connectWalletPrompt": "Ikonekta ang iyong wallet sa WalletConnect upang gumawa ng mga transaksyon", - "contact": "Makipag -ugnay", - "contact_list_contacts": "Mga contact", - "contact_list_wallets": "Ang mga wallets ko", - "contact_name": "pangalan ng contact", - "contact_support": "Makipag -ugnay sa suporta", + "contact": "Contact", + "contact_list_contacts": "Mga Contact", + "contact_list_wallets": "Mga Wallet Ko", + "contact_name": "Pangalan ng Contact", + "contact_support": "Makipag-ugnay sa Suporta", "continue_text": "Magpatuloy", "contractName": "Pangalan ng Kontrata", "contractSymbol": "Simbolo ng Kontrata", - "copied_key_to_clipboard": "Kinopya ang ${key} sa clipboard", - "copied_to_clipboard": "Kinopya sa clipboard", - "copy": "Kopya", - "copy_address": "Kopyahin ang address", - "copy_id": "Kopyahin ang id", + "copied_key_to_clipboard": "Kinopya ang ${key} sa Clipboard", + "copied_to_clipboard": "Kinopya sa Clipboard", + "copy": "Kopyahin", + "copy_address": "Kopyahin ang Address", + "copy_id": "Kopyahin ang ID", "copyWalletConnectLink": "Kopyahin ang link ng WalletConnect mula sa dApp at i-paste dito", "countries": "Mga bansa", - "create_account": "Lumikha ng account", - "create_backup": "Gumawa ng backup", + "create_account": "Lumikha ng Account", + "create_backup": "Lumikha ng backup", "create_donation_link": "Lumikha ng link ng donasyon", "create_invoice": "Lumikha ng invoice", - "create_new": "Lumikha ng bagong pitaka", - "create_new_account": "Lumikha ng Bagong Account", - "creating_new_wallet": "Lumilikha ng bagong pitaka", + "create_new": "Lumikha ng Bagong Wallet", + "create_new_account": "Lumikha ng bagong account", + "creating_new_wallet": "Lumikha ng bagong wallet", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Petsa ng paglikha", - "custom": "pasadya", - "custom_drag": "Pasadyang (hawakan at i -drag)", - "custom_redeem_amount": "Pasadyang tinubos ang halaga", - "custom_value": "Pasadyang halaga", - "dark_theme": "Madilim", - "debit_card": "Debit card", - "debit_card_terms": "Ang pag -iimbak at paggamit ng numero ng iyong card ng pagbabayad (at mga kredensyal na naaayon sa iyong numero ng card ng pagbabayad) sa digital na pitaka na ito ay napapailalim sa mga termino at kundisyon ng naaangkop na kasunduan sa cardholder kasama ang nagbigay ng card ng pagbabayad, tulad ng sa oras -oras.", + "custom": "Pasadya", + "custom_drag": "Pasadya (Hawakan at I-drag)", + "custom_redeem_amount": "Pasadyang Tinubos ang Halaga", + "custom_value": "Pasadyang Halaga", + "dark_theme": "Dark", + "debit_card": "Debit Card", + "debit_card_terms": "Ang pag-iimbak at paggamit ng iyong numero sa card (at mga kredensyal na nauugnay sa numero ng iyong card sa pagbabayad) sa pagbabayad sa digital wallet na ito ay napapailalim sa mga tuntunin at kundisyon ng naaangkop na kasunduan sa may-ari ng card kasama ang nagbigay ng card ng pagbabayad, na may bisa sa pana-panahon.", "decimal_places_error": "Masyadong maraming mga lugar na desimal", - "decimals_cannot_be_zero": "Ang Token Decimal ay hindi maaaring maging zero.", - "default_buy_provider": "Default na Provider ng Pagbili", + "decimals_cannot_be_zero": "Ang token decimal ay hindi maaaring maging zero.", + "default_buy_provider": "Default na Buy Provider", "default_sell_provider": "Default na Sell Provider", "delete": "Tanggalin", - "delete_account": "Tanggalin ang account", - "delete_wallet": "Tanggalin ang pitaka", - "delete_wallet_confirm_message": "Sigurado ka bang nais mong tanggalin ang ${wallet_name} wallet?", + "delete_account": "Tanggalin ang Account", + "delete_wallet": "Tanggalin ang wallet", + "delete_wallet_confirm_message": "Sigurado ka ba na gusto mong tanggalin ang iyong ${wallet_name} wallet?", "deleteConnectionConfirmationPrompt": "Sigurado ka bang gusto mong tanggalin ang koneksyon sa", - "denominations": "Denominasyon", + "denominations": "Mga Denominasyon", "descending": "Pababang", "description": "Paglalarawan", "destination_tag": "Tag ng patutunguhan:", - "dfx_option_description": "Bumili ng crypto kasama ang EUR & CHF. Para sa mga customer at corporate customer sa Europa", + "dfx_option_description": "Bumili ng crypto kasama ang EUR & CHF. Para sa mga retail customer at corporate customer sa Europe", "didnt_get_code": "Hindi nakuha ang code?", - "digit_pin": "-digit pin", - "digital_and_physical_card": "Digital at Physical Prepaid Debit Card", + "digit_pin": "-digit PIN", + "digital_and_physical_card": " digital at pisikal na prepaid debit card", "disable": "Huwag paganahin", - "disable_bulletin": "Huwag paganahin ang Bulletin ng Katayuan ng Serbisyo", + "disable_bulletin": "Huwag paganahin ang bulletin ng katayuan ng serbisyo", "disable_buy": "Huwag paganahin ang pagkilos ng pagbili", - "disable_cake_2fa": "Huwag paganahin ang cake 2FA", + "disable_cake_2fa": "Huwag paganahin ang Cake 2FA", "disable_exchange": "Huwag paganahin ang palitan", - "disable_fiat": "Huwag paganahin ang Fiat", + "disable_fiat": "Huwag paganahin ang fiat", "disable_sell": "Huwag paganahin ang pagkilos ng pagbebenta", - "disableBatteryOptimization": "Huwag paganahin ang pag -optimize ng baterya", - "disableBatteryOptimizationDescription": "Nais mo bang huwag paganahin ang pag -optimize ng baterya upang gawing mas malaya at maayos ang pag -sync ng background?", + "disableBatteryOptimization": "Huwag Paganahin ang Pag-optimize ng Baterya", + "disableBatteryOptimizationDescription": "Nais mo bang huwag paganahin ang pag-optimize ng baterya upang gawing mas malaya at maayos ang background sync?", "disabled": "Hindi pinagana", "discount": "Makatipid ng ${value}%", "display_settings": "Mga setting ng pagpapakita", "displayable": "Maipapakita", - "do_not_have_enough_gas_asset": "Wala kang sapat na ${currency} para gumawa ng transaksyon sa kasalukuyang kundisyon ng network ng blockchain. Kailangan mo ng higit pang ${currency} upang magbayad ng mga bayarin sa network ng blockchain, kahit na nagpapadala ka ng ibang asset.", - "do_not_send": "Huwag magpadala", - "do_not_share_warning_text": "Huwag ibahagi ang mga ito sa sinumang iba pa, kabilang ang suporta.\n\nAng iyong mga pondo ay maaari at ninakaw!", + "do_not_have_enough_gas_asset": "Wala kang sapat na ${currency} para gumawa ng transaksyon sa kasalukuyang kundisyon ng network ng blockchain. Kailangan mo ng higit pang ${currency} upang magbayad ng mga fee sa network ng blockchain, kahit na nagpapadala ka ng ibang asset.", + "do_not_send": "Huwag ipadala", + "do_not_share_warning_text": "Huwag ibahagi ang mga ito sa sinuman kasama ang tagatustos.\n\nMaaaring manakaw ang iyong mga pondo!", "do_not_show_me": "Huwag mo itong ipakita muli", "domain_looks_up": "Mga paghahanap ng domain", - "donation_link_details": "Mga Detalye ng Link ng Donasyon", - "e_sign_consent": "E-sign na pahintulot", - "edit": "I -edit", - "edit_backup_password": "I -edit ang backup password", - "edit_node": "I -edit ang node", - "edit_token": "I -edit ang token", - "electrum_address_disclaimer": "Bumubuo kami ng mga bagong address sa tuwing gumagamit ka ng isa, ngunit ang mga nakaraang address ay patuloy na gumagana", - "email_address": "Email address", - "enable_replace_by_fee": "Paganahin ang palitan-by-fee", - "enable_silent_payments_scanning": "Paganahin ang pag -scan ng tahimik na pagbabayad", + "donation_link_details": "Mga detalye ng link ng donasyon", + "e_sign_consent": "E-Sign Consent", + "edit": "I-edit", + "edit_backup_password": "I-edit ang backup na password", + "edit_node": "I-edit ang Node", + "edit_token": "I-edit ang token", + "electrum_address_disclaimer": "Bumubuo kami ng mga bagong address sa tuwing gagamit ka ng isa, ngunit ang mga nakaraang address ay patuloy na gumagana", + "email_address": "Email Address", + "enable_replace_by_fee": "Paganahin ang Replace-By-Fee", + "enable_silent_payments_scanning": "Paganahin ang pag-scan ng mga tahimik na pagbabayad", "enabled": "Pinagana", "enter_amount": "Ipasok ang halaga", - "enter_backup_password": "Ipasok ang backup password dito", + "enter_backup_password": "Ipasok ang backup na password dito", "enter_code": "Ipasok ang code", - "enter_seed_phrase": "Ipasok ang iyong pariralang binhi", - "enter_totp_code": "Mangyaring ipasok ang TOTP code.", - "enter_wallet_password": "Ipasok ang password ng pitaka", + "enter_seed_phrase": "Ipasok ang iyong seed phrase", + "enter_totp_code": "Ipasok ang TOTP code", + "enter_wallet_password": "Ipasok ang password ng wallet", "enter_your_note": "Ipasok ang iyong tala ...", - "enter_your_pin": "Ipasok ang iyong pin", - "enter_your_pin_again": "Ipasok muli ang iyong pin", - "enterTokenID": "Ilagay ang token ID", - "enterWalletConnectURI": "Ilagay ang WalletConnect URI", + "enter_your_pin": "Ipasok ang iyong PIN", + "enter_your_pin_again": "Ipasok muli ang iyong PIN", + "enterTokenID": "Ipasok ang token ID", + "enterWalletConnectURI": "Ipasok ang WalletConnect URI", "error": "Error", - "error_dialog_content": "Oops, nakakuha kami ng ilang error.\n\nMangyaring ipadala ang ulat ng pag -crash sa aming koponan ng suporta upang maging mas mahusay ang application.", + "error_dialog_content": "Oops, nakakuha kami ng ilang error.\n\nMangyaring ipadala ang crash report sa aming koponan ng suporta upang maging mas mahusay ang application.", "error_text_account_name": "Ang pangalan ng account ay maaari lamang maglaman ng mga titik, numero\nat dapat sa pagitan ng 1 at 15 character ang haba", "error_text_address": "Ang wallet address ay dapat na tumutugma sa uri\nng cryptocurrency", "error_text_amount": "Ang halaga ay maaari lamang maglaman ng mga numero", - "error_text_contact_name": "Ang pangalan ng contact ay hindi maaaring maglaman ng mga simbolo ng ',' \"\nat dapat sa pagitan ng 1 at 32 character ang haba", - "error_text_crypto_currency": "Ang bilang ng mga numero ng fraction\ndapat mas mababa o katumbas ng 12", - "error_text_fiat": "Ang halaga ng halaga ay hindi maaaring lumampas sa magagamit na balanse.\nAng bilang ng mga numero ng fraction ay dapat na mas mababa o katumbas ng 2", + "error_text_contact_name": "Ang pangalan ng contact ay hindi maaaring maglaman ng ` , ' \" mga symbolo\nat dapat nasa pagitan ng 1 at 32 character ang haba", + "error_text_crypto_currency": "Ang bilang ng mga fraction digit\nay dapat mas mababa o katumbas ng 12", + "error_text_fiat": "Ang halaga ay hindi maaaring lumampas sa magagamit na balanse.\nang bilang ng mga fraction digit ay dapat na mas kaunti o katumbas ng 2", "error_text_input_above_maximum_limit": "Ang halaga ay higit pa sa maximum", "error_text_input_below_minimum_limit": "Ang halaga ay mas mababa sa minimum", - "error_text_keys": "Ang mga susi ng wallet ay maaari lamang maglaman ng 64 chars sa hex", - "error_text_limits_loading_failed": "Ang kalakalan para sa ${provider} ay hindi nilikha. Nabigo ang mga limitasyon sa paglo -load", - "error_text_maximum_limit": "Ang kalakalan para sa ${provider} ay hindi nilikha. Ang halaga ay higit na maximum: ${max} ${currency}", - "error_text_minimal_limit": "Ang kalakalan para sa ${provider} ay hindi nilikha. Ang halaga ay mas mababa pagkatapos ng minimal: ${min} ${currency}", - "error_text_node_address": "Mangyaring magpasok ng isang address ng IPv4", - "error_text_node_port": "Ang Node Port ay maaari lamang maglaman ng mga numero sa pagitan ng 0 at 65535", - "error_text_node_proxy_address": "Mangyaring ipasok ang : , halimbawa 127.0.0.1:9050", - "error_text_payment_id": "Ang Payment ID ay maaari lamang maglaman mula 16 hanggang 64 chars sa hex", + "error_text_keys": "Ang mga wallet key ay maaari lamang maglaman ng 64 chars sa hex", + "error_text_limits_loading_failed": "Ang kalakalan para sa ${provider} hindi nilikha . Nabigo ang pag-load ng mga limitasyon", + "error_text_maximum_limit": "Ang kalakalan para sa ${provider} ay hindi nilikha. Ang halaga ay higit sa maximum: ${max} ${currency}", + "error_text_minimal_limit": "Ang kalakalan para sa ${provider} ay hindi nilikha. Ang halaga ay mas mababa sa minimum: ${min} ${currency}", + "error_text_node_address": "Pakipasok isan iPv4 address", + "error_text_node_port": "Ang node port ay maaari lamang maglaman ng numero sa pagitan ng 0 at 65535", + "error_text_node_proxy_address": "Pakipasok : , halimbawa 127.0.0.1:9050", + "error_text_payment_id": "Ang Payment ID ay dapat maglaman ng 16 na char sa hex", "error_text_subaddress_name": "Ang pangalan ng subaddress ay hindi maaaring maglaman ng mga simbolo na `, '\"\nat dapat sa pagitan ng 1 at 20 character ang haba", "error_text_template": "Ang pangalan ng template at address ay hindi maaaring maglaman ng mga simbolo ng ',' \"\nat dapat sa pagitan ng 1 at 106 na character ang haba", - "error_text_wallet_name": "Ang pangalan ng pitaka ay maaari lamang maglaman ng mga titik, numero, _ - mga simbolo\nat dapat sa pagitan ng 1 at 33 character ang haba", + "error_text_wallet_name": "Ang pangalan ng wallet ay maaari lamang maglaman ng mga titik, numero, _ - mga simbolo\nat dapat sa pagitan ng 1 at 33 character ang haba", "error_text_xmr": "Ang halaga ng XMR ay hindi maaaring lumampas sa magagamit na balanse.\nAng bilang ng mga numero ng fraction ay dapat na mas mababa o katumbas ng 12", "errorGettingCredentials": "Nabigo: Error habang kumukuha ng mga kredensyal", - "errorSigningTransaction": "May naganap na error habang pinipirmahan ang transaksyon", + "errorSigningTransaction": "Error habang pinipirmahan ang transaksyon", "estimated": "Tinatayang", - "estimated_new_fee": "Tinatayang bagong bayad", + "estimated_new_fee": "Tinatayang bagong fee", "estimated_receive_amount": "Tinatayang natanggap na halaga", "etherscan_history": "Kasaysayan ng Etherscan", "event": "Kaganapan", "events": "Mga kaganapan", "exchange": "Palitan", - "exchange_incorrect_current_wallet_for_xmr": "Kung nais mong makipagpalitan ng XMR mula sa iyong balanse ng cake wallet Monero, mangyaring lumipat sa iyong Monero Wallet muna.", + "exchange_incorrect_current_wallet_for_xmr": "Kung gusto mong palitan ang XMR mula sa iyong balanse ng Monero ng Cake Wallet, mangyaring lumipat muna sa iyong Monero wallet.", "exchange_new_template": "Bagong template", "exchange_provider_unsupported": "Ang ${providerName} ay hindi na suportado!", - "exchange_result_confirm": "Sa pamamagitan ng pagpindot ng kumpirmahin, magpapadala ka ng ${fetchingLabel} ${from} mula sa iyong pitaka na tinatawag na ${walletName} sa address na ipinakita sa ibaba. O maaari kang magpadala mula sa iyong panlabas na pitaka sa ibaba address/qr code.\n\nMangyaring pindutin ang kumpirmahin na magpatuloy o bumalik upang baguhin ang mga halaga.", - "exchange_result_description": "Dapat kang magpadala ng isang minimum na ${fetchingLabel} ${from} hanggang sa address na ipinakita sa susunod na pahina. Kung nagpapadala ka ng isang halaga na mas mababa kaysa sa ${fetchingLabel} ${from} maaaring hindi ito ma -convert at maaaring hindi ito ibabalik.", - "exchange_result_write_down_ID": "*Mangyaring kopyahin o isulat ang iyong ID na ipinakita sa itaas.", + "exchange_result_confirm": "Sa pamamagitan ng pagpindot sa kumpirmahin, ikaw ay magpapadala ${fetchingLabel} ${from} mula sa inyong wallet na tinatawag ${walletName} sa wallet na ipinapakita sa ibaba. O pwede kang magpadala sa inyong external wallet sa ibabang address/QR code.\n\nPara magpatuloy, mangyaring pindutin upang kumpirmahin o bumalik para baguhin ang halaga.", + "exchange_result_description": "Kailangan mong magpadala ng minimum ${fetchingLabel} ${from} sa address na ipinakita sa susunod na pahina. Kung magpapadala ka ng halagang mas masmababa sa ${fetchingLabel} ${from} maaring hindi ito ma-convert at maaaring hindi ito ma-refund.", + "exchange_result_write_down_ID": "*Mangyaring kopyahin o isulat ang inyong ID na ipinapakita sa itaas.", "exchange_result_write_down_trade_id": "Mangyaring kopyahin o isulat ang trade ID upang magpatuloy.", - "exchange_sync_alert_content": "Mangyaring maghintay hanggang ang iyong pitaka ay naka -synchronize", - "expired": "Nag -expire", - "expires": "Mag -expire", + "exchange_sync_alert_content": "Mangyaring maghintay hanggang ang iyong wallet ay naka-synchronize", + "expired": "Nag-expire na", + "expires": "Mag-e-expire", "expiresOn": "Mag-e-expire sa", - "expiry_and_validity": "Pag -expire at bisa", - "export_backup": "I -export ang backup", + "expiry_and_validity": "Pag-expire at Bisa", + "export_backup": "I-export ang backup", "extra_id": "Dagdag na ID:", "extracted_address_content": "Magpapadala ka ng pondo sa\n${recipient_name}", - "failed_authentication": "Nabigong pagpapatunay. ${state_error}", + "failed_authentication": "Nabigo ang pagpapatunay. ${state_error}", "faq": "FAQ", "features": "Mga tampok", "fetching": "Pagkuha", "fiat_api": "Fiat API", "fiat_balance": "Balanse ng fiat", "field_required": "Kinakailangan ang patlang na ito", - "fill_code": "Mangyaring punan ang verification code na ibinigay sa iyong email", + "fill_code": "Mangyaring ilagay ang verfification code na ibinigay sa iyong email", "filter_by": "Filter ni", - "first_wallet_text": "Kahanga -hangang pitaka para sa Monero, Bitcoin, Ethereum, Litecoin, at Haven", + "first_wallet_text": "Kahanga-hangang wallet para sa Monero, Bitcoin, Litecoin, Ethereum, at Haven", "fixed_pair_not_supported": "Ang nakapirming pares na ito ay hindi suportado sa mga napiling palitan", - "fixed_rate": "Naayos na rate", - "fixed_rate_alert": "Magagawa mong ipasok ang makatanggap na halaga kapag naka -check ang naayos na mode ng rate. Nais mo bang lumipat sa nakapirming mode ng rate?", - "forgot_password": "Nakalimutan ang password", - "freeze": "I -freeze", - "frequently_asked_questions": "Madalas na nagtanong", + "fixed_rate": "Fixed rate", + "fixed_rate_alert": "Makakapagpasok ka ng halaga ng pagtanggap kapag nasuri ang fixed rate mode. Gusto mo bang lumipat sa fixed rate mode?", + "forgot_password": "Nakalimutan ang Password", + "freeze": "I-freeze", + "frequently_asked_questions": "Mga madalas itanong", "frozen": "Frozen", - "full_balance": "Buong balanse", + "full_balance": "Buong Balanse", "generate_name": "Bumuo ng pangalan", "generating_gift_card": "Bumubuo ng Gift Card", - "get_a": "Kumuha ng", - "get_card_note": "na maaari mong i -reload sa mga digital na pera. Walang karagdagang impormasyon na kailangan!", - "get_your_yat": "Kunin ang iyong yat", + "get_a": "Kumuha ng ", + "get_card_note": " na maaari mong i-load gamit ang mga digital na pera. Walang karagdagang impormasyon na kailangan!", + "get_your_yat": "Kunin ang iyong Yat", "gift_card_amount": "Halaga ng Gift Card", - "gift_card_balance_note": "Ang mga kard ng regalo na may natitirang balanse ay lilitaw dito", - "gift_card_is_generated": "Ang card ng regalo ay nabuo", - "gift_card_number": "Numero ng regalo card", - "gift_card_redeemed_note": "Ang mga kard ng regalo na iyong tinubos ay lilitaw dito", - "gift_cards": "Mga kard ng regalo", - "gift_cards_unavailable": "Magagamit ang mga gift card para sa pagbili lamang kasama ang Monero, Bitcoin, at Litecoin sa oras na ito", + "gift_card_balance_note": "Lalabas dito ang mga gift card na may natitirang balanse", + "gift_card_is_generated": "Nabuo ang gift card", + "gift_card_number": "Numero ng gift card", + "gift_card_redeemed_note": "Lalabas dito ang mga gift card na na-redeem mo", + "gift_cards": "Mga Gift Card", + "gift_cards_unavailable": "Ang mga gift card ay magagamit lamang para bilhin gamit ng Monero, Bitcoin, at Litecoin sa ngayon", "got_it": "Nakuha ko", - "gross_balance": "Balanse ng gross", + "gross_balance": "Kabuuang balanse", "group_by_type": "Pangkat ayon sa uri", - "haven_app": "Haven sa pamamagitan ng cake wallet", - "haven_app_wallet_text": "Galing ng pitaka para sa Haven", + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Kahanga-hangang wallet para sa Haven", "help": "Tulong", - "hidden_balance": "Nakatagong balanse", + "hidden_balance": "Nakatagong Balanse", "hide_details": "Itago ang mga detalye", - "high_contrast_theme": "Mataas na tema ng kaibahan", + "high_contrast_theme": "High Contrast Theme", "home_screen_settings": "Mga setting ng home screen", "how_to_use": "Paano gamitin", - "how_to_use_card": "Paano gamitin ang kard na ito", - "id": "ID:", + "how_to_use_card": "Paano gamitin ang card na ito", + "id": "ID: ", "ignor": "Huwag pansinin", - "import": "Angkat", + "import": "Mag-import", "importNFTs": "Mag-import ng mga NFT", - "in_store": "Nakatago", + "in_store": "Nasa Stock", "incoming": "Papasok", - "incorrect_seed": "Ang teksto na ipinasok ay hindi wasto.", + "incorrect_seed": "Ang text na ipinasok ay hindi wasto.", "inputs": "Mga input", - "insufficientFundsForRentError": "Wala kang sapat na sol upang masakop ang bayad sa transaksyon at upa para sa account. Mabait na magdagdag ng higit pa sa iyong pitaka o bawasan ang halaga ng sol na iyong ipinapadala", - "introducing_cake_pay": "Ipinakikilala ang cake pay!", - "invalid_input": "Di -wastong input", - "invalid_password": "Di wastong password", + "insufficientFundsForRentError": "Wala kang sapat na SOL upang masakop ang fee sa transaksyon at upa para sa account. Mabait na magdagdag ng higit pa sa iyong wallet o bawasan ang halaga ng SOL na iyong ipinapadala", + "introducing_cake_pay": "Pagpapakilala ng Cake Pay!", + "invalid_input": "Di-wastong input", + "invalid_password": "Di-wastong password", "invoice_details": "Mga detalye ng invoice", "is_percentage": "ay", - "last_30_days": "Huling 30 araw", + "last_30_days": "Huling 30 na araw", "learn_more": "Matuto nang higit pa", - "ledger_connection_error": "Nabigong kumonekta sa iyo ledger. Pakisubukang muli.", - "ledger_error_device_locked": "Naka -lock ang ledger", - "ledger_error_tx_rejected_by_user": "Ang transaksyon ay tinanggihan sa aparato", - "ledger_error_wrong_app": "Mangyaring tiyaking pinipili mo ang tamang app sa iyong ledger", - "ledger_please_enable_bluetooth": "Mangyaring paganahin ang Bluetooth upang makita ang iyong ledger", - "light_theme": "Ilaw", - "load_more": "Mag -load pa", - "loading_your_wallet": "Naglo -load ng iyong pitaka", - "login": "Mag log in", - "logout": "Mag -logout", - "low_fee": "Mababang bayad", - "low_fee_alert": "Kasalukuyan kang gumagamit ng isang mababang priyoridad sa bayad sa network. Maaari itong maging sanhi ng mahabang paghihintay, iba't ibang mga rate, o kanselahin ang mga trading. Inirerekumenda namin ang pagtatakda ng isang mas mataas na bayad para sa isang mas mahusay na karanasan.", + "ledger_connection_error": "Nabigong kumonekta sa iyong Ledger. Pakisubukang muli.", + "ledger_error_device_locked": "Naka-lock ang Ledger", + "ledger_error_tx_rejected_by_user": "Ang transaksyon ay tinanggihan sa hardware wallet", + "ledger_error_wrong_app": "Mangyaring tiyaking pinipili mo ang tamang app sa iyong Ledger", + "ledger_please_enable_bluetooth": "Mangyaring paganahin ang Bluetooth upang makita ang iyong Ledger", + "light_theme": "Light", + "load_more": "Mag-load pa", + "loading_your_wallet": "Naglo-load ng iyong wallet", + "login": "Mag-login", + "logout": "Mag-logout", + "low_fee": "Mababang fee", + "low_fee_alert": "Kasalukuyan kang gumagamit ng isang mababang priyoridad sa network fee. Maaari itong maging sanhi ng mahabang paghihintay, iba't ibang mga rate, o kanselahin ang mga trading. Inirerekumenda namin ang pagtatakda ng isang mas mataas na fee para sa isang mas mahusay na karanasan.", "manage_nodes": "Pamahalaan ang mga node", - "manage_pow_nodes": "Pamahalaan ang mga POW node", - "manage_yats": "Pamahalaan ang mga yats", + "manage_pow_nodes": "Pamahalaan ang mga PoW node", + "manage_yats": "Pamahalaan ang mga Yat", "mark_as_redeemed": "Markahan bilang tinubos", "market_place": "Marketplace", - "matrix_green_dark_theme": "Matrix Green Madilim na Tema", + "matrix_green_dark_theme": "Matrix Green Dark Theme", "max_amount": "Max: ${value}", "max_value": "Max: ${value} ${currency}", "memo": "Memo:", "message": "Mensahe", - "methods": "Paraan", + "methods": "Mga Paraan", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", "minutes_to_pin_code": "${minute} minuto", - "mm": "Mm", - "modify_2fa": "Baguhin ang cake 2FA", - "monero_com": "Monero.com sa pamamagitan ng cake wallet", - "monero_com_wallet_text": "Galing ng pitaka para sa Monero", - "monero_dark_theme": "Monero Madilim na Tema", - "monero_light_theme": "Tema ng Monero Light", - "moonpay_alert_text": "Ang halaga ng halaga ay dapat na higit pa o katumbas ng ${minAmount} ${fiatCurrency}", - "more_options": "Higit pang mga pagpipilian", + "mm": "MM", + "modify_2fa": "Baguhin ang Cake 2FA", + "monero_com": "Monero.com by Cake Wallet", + "monero_com_wallet_text": "Kahanga-hangang wallet para sa Monero", + "monero_dark_theme": "Monero Dark Theme", + "monero_light_theme": "Monero Light Theme", + "moonpay_alert_text": "Ang halaga ay dapat na higit pa o katumbas ng ${minAmount} ${fiatCurrency}", + "more_options": "Higit pang mga Pagpipilian", "name": "Pangalan", - "nano_current_rep": "Kasalukuyang kinatawan", - "nano_gpt_thanks_message": "Salamat sa paggamit ng nanogpt! Tandaan na bumalik sa browser matapos makumpleto ang iyong transaksyon!", - "nano_pick_new_rep": "Pumili ng isang bagong kinatawan", - "nanogpt_subtitle": "Ang lahat ng mga pinakabagong modelo (GPT-4, Claude). \\ Nno subscription, magbayad gamit ang crypto.", + "nano_current_rep": "Kasalukuyang Representative", + "nano_gpt_thanks_message": "Salamat sa paggamit ng NanoGPT! Tandaan na bumalik sa browser matapos makumpleto ang iyong transaksyon!", + "nano_pick_new_rep": "Pumili ng isang bagong representative", + "nanogpt_subtitle": "Ang lahat ng mga pinakabagong modelo (GPT-4, Claude). \nNo subscription, magbayad gamit ang crypto.", "narrow": "Makitid", "new_first_wallet_text": "Panatilihing ligtas ang iyong crypto, piraso ng cake", - "new_node_testing": "Bagong pagsubok sa node", + "new_node_testing": "Bagong node testing", "new_subaddress_create": "Lumikha", "new_subaddress_label_name": "Pangalan ng label", - "new_subaddress_title": "Bagong tirahan", - "new_template": "Bagong template", - "new_wallet": "Bagong pitaka", + "new_subaddress_title": "Bagong address", + "new_template": "Bagong Template", + "new_wallet": "Bagong Wallet", "newConnection": "Bagong Koneksyon", - "no_cards_found": "Walang nahanap na mga kard", + "no_cards_found": "Walang nahanap na mga card", "no_id_needed": "Hindi kailangan ng ID!", - "no_id_required": "Walang kinakailangang ID. I -top up at gumastos kahit saan", + "no_id_required": "Hindi kailangan ng ID. I-top up at gumastos kahit saan", "no_relay_on_domain": "Walang relay para sa domain ng user o hindi available ang relay. Mangyaring pumili ng relay na gagamitin.", "no_relays": "Walang mga relay", "no_relays_message": "Nakakita kami ng Nostr NIP-05 record para sa user na ito, ngunit hindi ito naglalaman ng anumang mga relay. Mangyaring atasan ang tatanggap na magdagdag ng mga relay sa kanilang Nostr record.", "node_address": "Node address", "node_connection_failed": "Nabigo ang koneksyon", - "node_connection_successful": "Ang koneksyon ay matagumpay", - "node_new": "Bagong node", + "node_connection_successful": "Naging tagumpay ang konekyson", + "node_new": "Bagong Node", "node_port": "Node port", - "node_reset_settings_title": "I -reset ang Mga Setting", - "node_test": "Pagsusulit", - "nodes": "Node", - "nodes_list_reset_to_default_message": "Sigurado ka bang nais mong i -reset ang mga setting upang default?", - "none_of_selected_providers_can_exchange": "Wala sa mga napiling tagapagkaloob na maaaring gumawa ng palitan na ito", + "node_reset_settings_title": "I-reset ang mga settings", + "node_test": "Test", + "nodes": "Mga node", + "nodes_list_reset_to_default_message": "Sigurado ka bang gusto mo bang i-reset ang mga settings sa default?", + "none_of_selected_providers_can_exchange": "Wala sa mga napiling provider ang makakagawa ng palitan na ito", "noNFTYet": "Wala pang NFT", "normal": "Normal", - "note_optional": "Tandaan (Opsyonal)", - "note_tap_to_change": "Tandaan (Tapikin upang baguhin)", + "note_optional": "Tala (opsyonal)", + "note_tap_to_change": "Tala (i-tap para baguhin)", "nullURIError": "Ang URI ay null", - "offer_expires_in": "Mag -expire ang alok sa:", + "offer_expires_in": "Mag-expire ang alok sa: ", "offline": "Offline", - "ok": "Ok", - "old_fee": "Matandang bayad", - "onion_link": "Link ng Onion", + "ok": "OK", + "old_fee": "Dating fee", + "onion_link": "Onion link", "online": "Online", - "onramper_option_description": "Mabilis na bumili ng crypto na may maraming paraan ng pagbabayad. Available sa karamihan ng mga bansa. Iba-iba ang mga spread at bayarin.", + "onramper_option_description": "Mabilis na bumili ng crypto na may maraming paraan ng pagbabayad. Available sa karamihan ng mga bansa. Iba-iba ang mga spread at fee.", "open_gift_card": "Buksan ang Gift Card", "optional_description": "Opsyonal na paglalarawan", - "optional_email_hint": "Opsyonal na Payee Notification Email", + "optional_email_hint": "Opsyonal na payee notification email", "optional_name": "Opsyonal na pangalan ng tatanggap", - "optionally_order_card": "Opsyonal na mag -order ng isang pisikal na kard.", - "orbot_running_alert": "Mangyaring tiyakin na ang Orbot ay tumatakbo bago kumonekta sa node na ito.", + "optionally_order_card": "Opsyonal na mag-order ng pisikal na card.", + "orbot_running_alert": "Pakitiyak na tumatakbo ang Orbot bago kumonekta sa node na ito.", "order_by": "Iniutos ni", - "order_id": "Order id", - "order_physical_card": "Mag -order ng pisikal na kard", + "order_id": "Order ID", + "order_physical_card": "Mag-order ng Pisical na Card", "other_settings": "Iba pang mga setting", - "outdated_electrum_wallet_description": "Ang mga bagong wallets ng Bitcoin na nilikha sa cake ay mayroon na ngayong 24-salitang binhi. Ipinag-uutos na lumikha ka ng isang bagong pitaka ng Bitcoin at ilipat ang lahat ng iyong mga pondo sa bagong 24-salitang pitaka, at itigil ang paggamit ng mga pitaka na may 12-salitang binhi. Mangyaring gawin ito kaagad upang ma -secure ang iyong mga pondo.", - "outdated_electrum_wallet_receive_warning": "Kung ang pitaka na ito ay may 12-salitang binhi at nilikha sa cake, huwag magdeposito sa Bitcoin sa pitaka na ito. Ang anumang BTC na inilipat sa pitaka na ito ay maaaring mawala. Lumikha ng isang bagong 24-word wallet (tapikin ang menu sa kanang tuktok, piliin ang mga pitaka, piliin ang Lumikha ng Bagong Wallet, pagkatapos ay piliin ang Bitcoin) at agad na ilipat ang iyong BTC doon. Ang mga bagong (24-salita) BTC Wallets mula sa cake ay ligtas", + "outdated_electrum_wallet_description": "Ang mga bagong Bitcoin wallet na ginagawa sa Cake ay mayroon na ngayong 24 na salita na seed. Ipinag-uutos na lumikha ka ng bagong bitcoin wallet at ilipat ang lahat ng iyong pondo sa bagong 24-salitang wallet, at ihinto ang paggamit ng mga wallet na may 12-salitang seed. Mangyaring gawin ito kaagad upang ma-secure ang iyong mga pondo.", + "outdated_electrum_wallet_receive_warning": "Kung ang wallet na ito ay may 12-word seed na ginawa sa Cake, huwag magdeposito ng Bitcoin sa wallet na ito. Anumang BTC na inilipat sa wallet na ito ay maaaring mawala. Lumikha ng bagong 24 na salita na wallet (i-tap ang menu sa kanang taas, piliin ang Mga Wallets, piliin ang Lumikha ng Bagong Wallet, pagkatapos ay piliin ang Bitcoin) at agad na ilipat ang iyong BTC doon. Bagong (24 na salita) BTC wallet mula sa Cake ay ligtas", "outgoing": "Palabas", "outputs": "Mga output", - "overwrite_amount": "Overwrite na halaga", - "pairingInvalidEvent": "Pagpares ng Di-wastong Kaganapan", + "overwrite_amount": "I-overwrite ang halaga", + "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (opsyonal)", "password": "Password", - "paste": "I -paste", + "paste": "I-paste", "pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.", - "payment_id": "Payment ID:", - "payment_was_received": "Natanggap ang iyong pagbabayad.", - "pending": "(Pending)", + "payment_id": "Payment ID: ", + "payment_was_received": "Natanggap ang iyong bayad.", + "pending": "(hindi pa tapos)", "percentageOf": "ng ${amount}", - "pin_at_top": "Pin ${token} sa tuktok", - "pin_is_incorrect": "Mali ang pin", - "pin_number": "Numero ng pin", + "pin_at_top": "I-pin ${token} sa tuktok", + "pin_is_incorrect": "Mali ang PIN", + "pin_number": "Numero ng PIN", "placeholder_contacts": "Ang iyong mga contact ay ipapakita dito", "placeholder_transactions": "Ang iyong mga transaksyon ay ipapakita dito", - "please_fill_totp": "Mangyaring punan ang 8-digit na code na naroroon sa iyong iba pang aparato", - "please_make_selection": "Mangyaring gumawa ng isang pagpipilian sa ibaba upang lumikha o mabawi ang iyong pitaka.", - "please_reference_document": "Mangyaring sanggunian ang mga dokumento sa ibaba para sa karagdagang impormasyon.", + "please_fill_totp": "Mangyaring punan ang 8-digit na code na naroroon sa iyong iba pang device", + "please_make_selection": "Mangyaring gumawa ng isang pagpipilian sa ibaba upang lumikha o mabawi ang iyong wallet.", + "please_reference_document": "Mangyaring sumangguni sa mga dokumento sa ibaba para sa karagdagang impormasyon.", "please_select": "Pakipili:", - "please_select_backup_file": "Mangyaring piliin ang backup file at ipasok ang backup password.", - "please_try_to_connect_to_another_node": "Mangyaring subukang kumonekta sa isa pang node", + "please_select_backup_file": "Mangyaring piliin ang backup na file at ipasok ang backup na password.", + "please_try_to_connect_to_another_node": "Pakisubukang kumonekta sa iba pang node", "please_wait": "Mangyaring maghintay", "polygonscan_history": "Kasaysayan ng PolygonScan", - "powered_by": "Pinapagana ng ${title}", - "pre_seed_button_text": "Naiintindihan ko. Ipakita sa akin ang aking binhi", - "pre_seed_description": "Sa susunod na pahina makikita mo ang isang serye ng mga ${words} na mga salita. Ito ang iyong natatangi at pribadong binhi at ito ang tanging paraan upang mabawi ang iyong pitaka kung sakaling mawala o madepektong paggawa. Responsibilidad mong isulat ito at itago ito sa isang ligtas na lugar sa labas ng cake wallet app.", - "pre_seed_title": "Mahalaga", - "prepaid_cards": "Prepaid card", - "prevent_screenshots": "Maiwasan ang mga screenshot at pag -record ng screen", - "privacy": "Privacy", + "powered_by": "Pinapatakbo ng${title}", + "pre_seed_button_text": "Naiitindihan ko. Ipakita ang aking seed", + "pre_seed_description": "Sa susunod na pahina ay makikita mo ang isang serye ng ${words} na salita. Ito ang iyong natatangi at pribadong seed at ito ang tanging paraan upang mabawi ang iyong wallet kung sakaling mawala o hindi gumana. Responsibilidad mong isulat ito sa isang ligtas na lugar sa labas ng Cake Wallet app.", + "pre_seed_title": "MAHALAGA", + "prepaid_cards": "Mga Prepaid Card", + "prevent_screenshots": "Maiwasan ang mga screenshot at pag-record ng screen", + "privacy": "Pagkapribado", "privacy_policy": "Patakaran sa Pagkapribado", "privacy_settings": "Settings para sa pagsasa-pribado", - "private_key": "Pribadong susi", - "proceed_after_one_minute": "Kung ang screen ay hindi magpatuloy pagkatapos ng 1 minuto, suriin ang iyong email.", - "proceed_on_device": "Magpatuloy sa iyong aparato", - "proceed_on_device_description": "Mangyaring sundin ang mga tagubilin na sinenyasan sa iyong wallet ng hardware", + "private_key": "Private key", + "proceed_after_one_minute": "Kung ang screen ay hindi magpapatuloy pagkatapos ng 1 minuto, suriin ang iyong email.", + "proceed_on_device": "Magpatuloy sa iyong hardware wallet", + "proceed_on_device_description": "Mangyaring sundin ang mga tagubilin na sinenyasan sa iyong hardware wallet", "profile": "Profile", "provider_error": "${provider} error", - "public_key": "Pampublikong susi", + "public_key": "Public key", "purchase_gift_card": "Bumili ng Gift Card", - "purple_dark_theme": "Purple Madilim na Tema", - "qr_fullscreen": "Tapikin upang buksan ang buong screen QR code", - "qr_payment_amount": "Ang QR code na ito ay naglalaman ng isang halaga ng pagbabayad. Nais mo bang i -overwrite ang kasalukuyang halaga?", + "purple_dark_theme": "Purple Dark Theme", + "qr_fullscreen": "I-tap para makuha ang buong screen na QR code", + "qr_payment_amount": "Ang QR code na ito ay naglalaman ng halaga ng pagbabayad. Gusto mo bang i-overwrite ang kasalukuyang halaga?", "quantity": "Dami", - "question_to_disable_2fa": "Sigurado ka bang nais mong huwag paganahin ang cake 2fa? Ang isang 2FA code ay hindi na kinakailangan upang ma -access ang pitaka at ilang mga pag -andar.", + "question_to_disable_2fa": "Sigurado ka bang nais mong huwag paganahin ang Cake 2FA? Ang isang 2FA code ay hindi na kinakailangan upang ma-access ang wallet at ilang mga pag-andar.", "receivable_balance": "Natatanggap na balanse", "receive": "Tumanggap", "receive_amount": "Halaga", "received": "Natanggap", "recipient_address": "Address ng tatanggap", "reconnect": "Kumonekta muli", - "reconnect_alert_text": "Sigurado ka bang nais mong muling kumonekta?", - "reconnection": "Pag -ugnay muli", - "red_dark_theme": "Red Madilim na Tema", - "red_light_theme": "Red light tema", + "reconnect_alert_text": "Sigurado ka bang gusto mong kumonekta uli?", + "reconnection": "Muling pagkakakonekta", + "red_dark_theme": "Red Dark Theme", + "red_light_theme": "Red Light Theme", "redeemed": "Tinubos", - "refund_address": "Refund address", + "refund_address": "Address ng refund", "reject": "Tanggihan", "remaining": "natitira", "remove": "Alisin", "remove_node": "Alisin ang node", - "remove_node_message": "Sigurado ka bang nais mong alisin ang napiling node?", + "remove_node_message": "Sigurado ka bang gusto mong alisin ang napiling node?", "rename": "Palitan ang pangalan", - "rep_warning": "Babala ng kinatawan", - "rep_warning_sub": "Ang iyong kinatawan ay hindi lilitaw na nasa mabuting kalagayan. Tapikin dito upang pumili ng bago", - "repeat_wallet_password": "Ulitin ang password ng pitaka", - "repeated_password_is_incorrect": "Ang paulit -ulit na password ay hindi tama. Mangyaring ulitin muli ang password ng pitaka.", + "rep_warning": "Babala ng Representative", + "rep_warning_sub": "Ang iyong representative ay hindi lilitaw na nasa mabuting kalagayan. Tapikin dito upang pumili ng bago", + "repeat_wallet_password": "Ulitin ang password ng wallet", + "repeated_password_is_incorrect": "Ang paulit-ulit na password ay hindi tama. Mangyaring ulitin muli ang password ng wallet.", "require_for_adding_contacts": "Nangangailangan para sa pagdaragdag ng mga contact", "require_for_all_security_and_backup_settings": "Nangangailangan para sa lahat ng mga setting ng seguridad at backup", - "require_for_assessing_wallet": "Nangangailangan para sa pag -access ng pitaka", - "require_for_creating_new_wallets": "Nangangailangan para sa paglikha ng mga bagong pitaka", - "require_for_exchanges_to_external_wallets": "Kinakailangan para sa mga palitan sa mga panlabas na wallet", - "require_for_exchanges_to_internal_wallets": "Nangangailangan para sa mga palitan sa mga panloob na mga pitaka", + "require_for_assessing_wallet": "Nangangailangan para sa pag-access ng wallet", + "require_for_creating_new_wallets": "Nangangailangan para sa paglikha ng mga bagong wallet", + "require_for_exchanges_to_external_wallets": "Nangangailangan para sa mga palitan sa mga panlabas na wallet", + "require_for_exchanges_to_internal_wallets": "Nangangailangan para sa mga palitan sa mga panloob na wallet", "require_for_sends_to_contacts": "Nangangailangan para sa pagpapadala sa mga contact", - "require_for_sends_to_internal_wallets": "Nangangailangan para sa pagpapadala sa mga panloob na mga pitaka", + "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", - "rescan": "Rescan", - "resend_code": "Mangyaring ipagpatuloy ito", - "reset": "I -reset", - "reset_password": "I -reset ang password", - "restore_active_seed": "Aktibong binhi", + "require_pin_after": "Nangangailangan ng PIN pagkatapos", + "rescan": "Muling i-scan", + "resend_code": "Mangyaring ipadala ito muli", + "reset": "I-reset", + "reset_password": "I-reset ang password", + "restore_active_seed": "Aktibong seed", "restore_address": "Address", - "restore_bitcoin_description_from_keys": "Ibalik ang iyong pitaka mula sa nabuong wif string mula sa iyong mga pribadong susi", - "restore_bitcoin_description_from_seed": "Ibalik ang iyong pitaka mula sa 24 na code ng kombinasyon ng salita", + "restore_bitcoin_description_from_keys": "Ibalik ang iyong wallet mula sa nabuong WIF string mula sa iyong mga private key", + "restore_bitcoin_description_from_seed": "Ibalik ang iyong wallet mula sa 24 na salita na seed", "restore_bitcoin_title_from_keys": "Ibalik mula sa WIF", - "restore_description_from_backup": "Maaari mong ibalik ang buong cake wallet app mula sa iyong back-up file", - "restore_description_from_hardware_wallet": "Ibalik mula sa isang ledger hardware wallet", - "restore_description_from_keys": "Ibalik ang iyong pitaka mula sa nabuong mga keystroke na na -save mula sa iyong mga pribadong susi", - "restore_description_from_seed": "Ibalik ang iyong pitaka mula sa alinman sa 25 salita o 13 na code ng kombinasyon ng salita", - "restore_description_from_seed_keys": "Ibalik ang iyong pitaka mula sa mga binhi/susi na na -save mo upang ma -secure ang lugar", - "restore_from_date_or_blockheight": "Mangyaring magpasok ng isang petsa ng ilang araw bago mo nilikha ang pitaka na ito. O kung alam mo ang blockheight, mangyaring ipasok ito sa halip", - "restore_from_seed_placeholder": "Mangyaring ipasok o i -paste ang iyong binhi dito", - "restore_new_seed": "Bagong binhi", + "restore_description_from_backup": "Maari mong ibalik ang buong Cake Wallet app sa iyong backup file", + "restore_description_from_hardware_wallet": "Ibalik mula sa isang Ledger hardware wallet", + "restore_description_from_keys": "Ibalik ang iyong wallet mula sa nabuong mga keystrokes na na-save mula sa iyong mga private key", + "restore_description_from_seed": "Ibalik ang iyong wallet mula sa alinman sa 25 na salita o 13 na salita na seed", + "restore_description_from_seed_keys": "Ibalik ang inyong wallet mula sa inyong seed/keys na iyong na-save sa ligtas na lugar", + "restore_from_date_or_blockheight": "Mangyaring maglagay ng petsa ilang araw bago mo ginawa ang wallet na ito. O kung alam mo ang block height pwede ilagay ito sa halip", + "restore_from_seed_placeholder": "Mangyaring ipasok o idikit ang iyong seed dito", + "restore_new_seed": "Bagong seed", "restore_next": "Susunod", "restore_recover": "Ibalik", - "restore_restore_wallet": "Ibalik ang pitaka", - "restore_seed_keys_restore": "Ibinalik ang mga binhi/susi", - "restore_spend_key_private": "Gumastos ng susi (pribado)", + "restore_restore_wallet": "Ibalik ang wallet", + "restore_seed_keys_restore": "Ibalik mula sa Seed/Keys", + "restore_spend_key_private": "Spend key (private)", "restore_title_from_backup": "Ibalik mula sa backup", - "restore_title_from_hardware_wallet": "Ibalik mula sa pitaka ng hardware", - "restore_title_from_keys": "Ibalik mula sa mga susi", - "restore_title_from_seed": "Ibalik mula sa binhi", - "restore_title_from_seed_keys": "Ibalik mula sa mga binhi/susi", - "restore_view_key_private": "Tingnan ang Key (Pribado)", - "restore_wallet": "Ibalik ang pitaka", - "restore_wallet_name": "Pangalan ng Wallet", - "restore_wallet_restore_description": "Paglalarawan ng Wallet", + "restore_title_from_hardware_wallet": "Ibalik mula sa hardware wallet", + "restore_title_from_keys": "Ibalik mula sa keys", + "restore_title_from_seed": "Ibalik mula sa seed", + "restore_title_from_seed_keys": "Ibalik mula sa seed/keys", + "restore_view_key_private": "View key (private)", + "restore_wallet": "Ibalik ang wallet", + "restore_wallet_name": "Pangalan ng wallet", + "restore_wallet_restore_description": "Paglalarawan ng pagpapanumbalik ng wallet", "robinhood_option_description": "Bumili at ilipat kaagad gamit ang iyong debit card, bank account, o balanse ng Robinhood. USA lang.", - "router_no_route": "Walang ruta na tinukoy para sa ${name}", - "save": "I -save", - "save_backup_password": "Mangyaring tiyaking nai -save mo ang iyong backup password. Hindi mo mai -import ang iyong mga backup na file nang wala ito.", - "save_backup_password_alert": "I -save ang backup password", - "save_to_downloads": "I -save sa mga pag -download", - "saved_the_trade_id": "Nai -save ko ang trade ID", - "scan_one_block": "I -scan ang isang bloke", - "scan_qr_code": "I -scan ang QR Code", - "scan_qr_code_to_get_address": "I -scan ang QR code upang makuha ang address", + "router_no_route": "Walang tinukoy na ruta para sa ${name}", + "save": "I-save", + "save_backup_password": "Pakitiyak na nai-save mo ang iyong backup na password. Hindi mo mai-import ang iyong mga backup na file kun wala ito.", + "save_backup_password_alert": "I-save ang backup na password", + "save_to_downloads": "I-save sa mga Pag-download", + "saved_the_trade_id": "Nai-save ko na ang trade ID", + "scan_one_block": "I-scan ang isang bloke", + "scan_qr_code": "I-scan ang QR code", + "scan_qr_code_to_get_address": "I-scan ang QR code upang makuha ang address", "scan_qr_on_device": "I-scan ang QR code na ito sa ibang device", "search": "Maghanap", "search_add_token": "Maghanap / Magdagdag ng Token", "search_category": "Kategorya ng paghahanap", "search_currency": "Maghanap ng pera", "search_language": "Maghanap ng wika", - "second_intro_content": "Ang iyong yat ay isang solong natatanging address ng emoji na pumapalit sa lahat ng iyong mahabang hexadecimal address para sa lahat ng iyong mga pera.", - "second_intro_title": "Isang address ng emoji upang mamuno sa kanilang lahat", + "second_intro_content": "Ang iyong Yat ay isang natatanging emoji address na pumapalit sa lahat ng iyong mahabang hexadecimal address para sa lahat ng iyong pera.", + "second_intro_title": "Isang emoji address para pamunuan silang lahat", "security_and_backup": "Seguridad at backup", - "seed_alert_back": "Bumalik ka", - "seed_alert_content": "Ang binhi ay ang tanging paraan upang mabawi ang iyong pitaka. Nasulat mo na ba ito?", - "seed_alert_title": "Pansin", + "seed_alert_back": "Bumalik", + "seed_alert_content": "Ang seed ay ang tanging paraan upang mabawi ang iyong wallet. Naisulat mo na ba?", + "seed_alert_title": "Attention", "seed_alert_yes": "Oo meron ako", - "seed_choose": "Pumili ng wika ng binhi", + "seed_choose": "Pumili ng seed language", "seed_hex_form": "Wallet seed (hex form)", - "seed_key": "Seed Key", - "seed_language": "Wika ng binhi", - "seed_language_chinese": "Tsino", - "seed_language_chinese_traditional": "Intsik (tradisyonal)", + "seed_key": "Seed key", + "seed_language": "Wika ng seed", + "seed_language_chinese": "Chinese", + "seed_language_chinese_traditional": "Chinese (Traditional)", "seed_language_czech": "Czech", "seed_language_dutch": "Dutch", - "seed_language_english": "Ingles", - "seed_language_french": "Pranses", - "seed_language_german": "Aleman", - "seed_language_italian": "Italyano", - "seed_language_japanese": "Hapon", + "seed_language_english": "English", + "seed_language_french": "French", + "seed_language_german": "German", + "seed_language_italian": "Italian", + "seed_language_japanese": "Japanese", "seed_language_korean": "Korean", "seed_language_next": "Susunod", - "seed_language_portuguese": "Portuges", + "seed_language_portuguese": "Portuguese", "seed_language_russian": "Russian", - "seed_language_spanish": "Espanyol", - "seed_phrase_length": "Haba ng parirala ng binhi", - "seed_reminder": "Mangyaring isulat ang mga ito kung sakaling mawala ka o punasan ang iyong telepono", - "seed_share": "Magbahagi ng binhi", - "seed_title": "Binhi", - "seedtype": "Seedtype", - "seedtype_legacy": "Pamana (25 salita)", + "seed_language_spanish": "Spanish", + "seed_phrase_length": "Haba ng parirala ng seed", + "seed_reminder": "Mangyaring isulat ang mga ito kung sakaling mawala o mabura sa inyong telepono", + "seed_share": "Ibahagi ang seed", + "seed_title": "Seed", + "seedtype": "Seed type", + "seedtype_legacy": "Legacy (25 na salita)", "seedtype_polyseed": "Polyseed (16 na salita)", - "select_backup_file": "Piliin ang backup file", + "select_backup_file": "Piliin ang backup na file", "select_buy_provider_notice": "Pumili ng provider ng pagbili sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na provider ng pagbili sa mga setting ng app.", - "select_destination": "Mangyaring piliin ang patutunguhan para sa backup file.", + "select_destination": "Mangyaring piliin ang patutunguhan para sa backup na file.", "select_sell_provider_notice": "Pumili ng provider ng nagbebenta sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na sell provider sa mga setting ng app.", "sell": "Ibenta", - "sell_alert_content": "Kasalukuyan lamang naming sinusuportahan ang pagbebenta ng Bitcoin, Ethereum at Litecoin. Mangyaring lumikha o lumipat sa iyong Bitcoin, Ethereum o Litecoin Wallet.", + "sell_alert_content": "Kasalukuyan lamang naming sinusuportahan ang pagbebenta ng Bitcoin, Ethereum at Litecoin. Mangyaring lumikha o lumipat sa iyong Bitcoin, Ethereum o Litecoin wallet.", "sell_monero_com_alert_content": "Ang pagbebenta ng Monero ay hindi pa suportado", "send": "Ipadala", "send_address": "${cryptoCurrency} address", "send_amount": "Halaga:", "send_creating_transaction": "Paglikha ng transaksyon", - "send_error_currency": "Ang pera ay maaari lamang maglaman ng mga numero", - "send_error_minimum_value": "Ang pinakamababang halaga ng halaga ay 0.01", - "send_estimated_fee": "Tinatayang bayad:", - "send_fee": "Bayad:", + "send_error_currency": "Ang halaga ay maaari lamang maglaman ng mga numero", + "send_error_minimum_value": "Ang minimum na halaga ay 0.01", + "send_estimated_fee": "Tinatayang fee:", + "send_fee": "Fee:", "send_name": "Pangalan", "send_new": "Bago", - "send_payment_id": "Payment ID (Opsyonal)", - "send_priority": "Sa kasalukuyan ang bayad ay nakatakda sa ${transactionPriority} priority.\nAng priority ng transaksyon ay maaaring maiakma sa mga setting", - "send_sending": "Pagpapadala ...", - "send_success": "Ang iyong ${crypto} ay matagumpay na naipadala", - "send_templates": "Mga template", + "send_payment_id": "Payment ID (opsyonal)", + "send_priority": "Kasalukuyang nakatakda ang fee sa ${transactionPriority} priyoridad.\n Ang priyoridad ng transaksyon ay maaaring isaayos sa mga setting", + "send_sending": "Nagpapadala...", + "send_success": "Matagumpay na naipadala ang iyong ${crypto}", + "send_templates": "Mga Template", "send_title": "Ipadala", - "send_to_this_address": "Magpadala ng ${currency} ${tag} sa address na ito", - "send_xmr": "Magpadala ng XMR", - "send_your_wallet": "Iyong pitaka", - "sending": "Pagpapadala", + "send_to_this_address": "Ipadala ang ${currency} ${tag} sa address na ito", + "send_xmr": "Ipadala ang XMR", + "send_your_wallet": "Iyong wallet", + "sending": "Nagpapadala", "sent": "Ipinadala", - "service_health_disabled": "Hindi pinagana ang Bulletin ng Serbisyo sa Kalusugan", - "service_health_disabled_message": "Ito ang pahina ng Bulletin ng Serbisyo ng Bulletin, maaari mong paganahin ang pahinang ito sa ilalim ng Mga Setting -> Pagkapribado", - "settings": "Mga setting", - "settings_all": "Lahat", - "settings_allow_biometrical_authentication": "Payagan ang pagpapatunay ng biometrical", + "service_health_disabled": "Hindi pinagana ang Service Health Bulletin", + "service_health_disabled_message": "Ito ang pahina ng Service Health Bulletin, maaari mong paganahin ang pahinang ito sa ilalim ng Mga Setting -> Pagkapribado", + "settings": "Mga Setting", + "settings_all": "LAHAT", + "settings_allow_biometrical_authentication": "Payagan ang biometrical authentication", "settings_can_be_changed_later": "Ang mga setting na ito ay maaaring mabago mamaya sa mga setting ng app", "settings_change_language": "Baguhin ang wika", - "settings_change_pin": "Baguhin ang pin", + "settings_change_pin": "Baguhin ang PIN", "settings_currency": "Pera", "settings_current_node": "Kasalukuyang node", - "settings_dark_mode": "Madilim na mode", + "settings_dark_mode": "Dark mode", "settings_display_balance": "Ipakita ang balanse", "settings_display_on_dashboard_list": "Ipakita sa listahan ng dashboard", - "settings_fee_priority": "Priority priority", - "settings_nodes": "Node", + "settings_fee_priority": "Priyoridad sa fee", + "settings_nodes": "Mga node", "settings_none": "Wala", - "settings_only_trades": "TRADES LAMANG", + "settings_only_trades": "Mga nangangalakal lamang", "settings_only_transactions": "Mga transaksyon lamang", "settings_personal": "Personal", - "settings_save_recipient_address": "I -save ang address ng tatanggap", + "settings_save_recipient_address": "I-save ang address ng tatanggap", "settings_support": "Suporta", "settings_terms_and_conditions": "Mga Tuntunin at Kundisyon", - "settings_title": "Mga setting", - "settings_trades": "Trading", - "settings_transactions": "Mga Transaksyon", - "settings_wallets": "Wallets", - "setup_2fa": "Setup cake 2fa", - "setup_2fa_text": "Gumagana ang Cake 2FA gamit ang TOTP bilang pangalawang kadahilanan sa pagpapatunay.\n\nAng TOTP ng Cake 2FA ay nangangailangan ng SHA-512 at 8 digit na suporta; nagbibigay ito ng mas mataas na seguridad. Higit pang impormasyon at suportadong app ang makikita sa gabay.", - "setup_pin": "Setup pin", - "setup_successful": "Matagumpay na na -set up ang iyong pin!", + "settings_title": "Mga Setting", + "settings_trades": "Mga kalakalan", + "settings_transactions": "Mga transaksyon", + "settings_wallets": "Mga wallet", + "setup_2fa": "Setup Cake 2FA", + "setup_2fa_text": "Gumagana ang Cake 2FA gamit ang TOTP bilang pangalawang kadahilanan sa pagpapatunay.\n\nAng TOTP ng Cake 2FA ay nangangailangan ng SHA-512 at 8 digit na suporta; nagbibigay ito ng mas mataas na seguridad. Higit pang impormasyon at suportadong app ang makikita sa guide.", + "setup_pin": "I-Setup ang PIN", + "setup_successful": "Matagumpay na na-set up ang iyong PIN!", "setup_totp_recommended": "I-setup ang TOTP", - "setup_warning_2fa_text": "Kakailanganin mong ibalik ang iyong wallet mula sa mnemonic seed.\n\nHindi ka matutulungan ng suporta sa cake kung mawawalan ka ng access sa iyong 2FA o mnemonic seeds.\nAng Cake 2FA ay pangalawang pagpapatotoo para sa ilang partikular na pagkilos sa wallet. Bago gamitin ang Cake 2FA, inirerekomenda naming basahin ang gabay.HINDI ito kasing-secure ng malamig na imbakan.\n\nKung nawalan ka ng access sa iyong 2FA app o TOTP keys, MAWAWALA ka ng access sa wallet na ito. ", - "setup_your_debit_card": "I -set up ang iyong debit card", + "setup_warning_2fa_text": "Ang Cake 2FA ay pangalawang pagpapatotoo para sa ilang partikular na pagkilos sa wallet. HINDI ito kasing-secure ng cold wallet.\n\nKung mawalan ka ng access sa iyong 2FA app o TOTP keys, MAWAWALA ka ng access sa wallet na ito. Kakailanganin mong i-restore ang iyong wallet mula sa mnemonic seed.\n\nHindi ka matutulungan ng Cake support kung mawawalan ka ng access sa iyong 2FA o mnemonic seeds.\nBago gamitin ang Cake 2FA, inirerekomenda naming basahin ang guide.", + "setup_your_debit_card": "I-set up ang iyong debit card", "share": "Ibahagi", "share_address": "Ibahagi ang address", "show_details": "Ipakita ang mga detalye", - "show_keys": "Ipakita ang mga binhi/susi", + "show_keys": "Ipakita ang mga seed/key", "show_market_place": "Ipakita ang Marketplace", - "show_seed": "Magpakita ng binhi", - "sign_up": "Mag -sign up", - "signTransaction": "Mag-sign Transaksyon", - "signup_for_card_accept_terms": "Mag -sign up para sa card at tanggapin ang mga termino.", + "show_seed": "Ipakita ang seed", + "sign_up": "Mag-sign Up", + "signTransaction": "Mag-sign ang Transaksyon", + "signup_for_card_accept_terms": "Mag-sign up para sa card at tanggapin ang mga tuntunin.", "silent_payments": "Tahimik na pagbabayad", - "silent_payments_always_scan": "Itakda ang mga tahimik na pagbabayad na laging nag -scan", + "silent_payments_always_scan": "Itakda ang mga tahimik na pagbabayad na laging nag-scan", "silent_payments_disclaimer": "Ang mga bagong address ay hindi mga bagong pagkakakilanlan. Ito ay isang muling paggamit ng isang umiiral na pagkakakilanlan na may ibang label.", "silent_payments_display_card": "Ipakita ang Silent Payment Card", - "silent_payments_scan_from_date": "I -scan mula sa petsa", - "silent_payments_scan_from_date_or_blockheight": "Mangyaring ipasok ang taas ng block na nais mong simulan ang pag -scan para sa papasok na tahimik na pagbabayad, o, gamitin ang petsa sa halip. Maaari kang pumili kung ang pitaka ay patuloy na pag -scan sa bawat bloke, o suriin lamang ang tinukoy na taas.", - "silent_payments_scan_from_height": "I -scan mula sa taas ng block", - "silent_payments_scanned_tip": "Na -scan sa tip! (${tip})", - "silent_payments_scanning": "Tahimik na pag -scan ng mga pagbabayad", + "silent_payments_scan_from_date": "I-scan mula sa petsa", + "silent_payments_scan_from_date_or_blockheight": "Mangyaring ipasok ang block height na gusto mong simulan ang pag-scan para sa papasok na tahimik na pagbabayad, o, gamitin ang petsa sa halip. Maaari kang pumili kung ang wallet ay patuloy na pag-scan sa bawat bloke, o suriin lamang ang tinukoy na taas.", + "silent_payments_scan_from_height": "I-scan mula sa block height", + "silent_payments_scanned_tip": "Na-scan sa tip! (${tip})", + "silent_payments_scanning": "Pag-scan ng tahimik na pagbabayad", "silent_payments_settings": "Mga setting ng tahimik na pagbabayad", "slidable": "Slidable", - "sort_by": "Pag -uri -uriin sa pamamagitan ng", - "spend_key_private": "Gumastos ng susi (pribado)", - "spend_key_public": "Gumastos ng susi (publiko)", - "status": "Katayuan:", + "sort_by": "Pag-uri-uriin sa pamamagitan ng", + "spend_key_private": "Spend key (private)", + "spend_key_public": "Spend key (public)", + "status": "Katayuan: ", "string_default": "Default", "subaddress_title": "Listahan ng Subaddress", - "subaddresses": "Mga Subaddresses", + "subaddresses": "Mga Subaddress", "submit_request": "magsumite ng isang kahilingan", "successful": "Matagumpay", "support_description_guides": "Dokumentasyon at suporta para sa mga karaniwang isyu", "support_description_live_chat": "Libre at mabilis! Ang mga bihasang kinatawan ng suporta ay magagamit upang tulungan", "support_description_other_links": "Sumali sa aming mga komunidad o maabot sa amin ang aming mga kasosyo sa pamamagitan ng iba pang mga pamamaraan", - "support_title_guides": "Mga Gabay sa Wallet ng cake", + "support_title_guides": "Mga guide sa Cake Wallet", "support_title_live_chat": "Live na suporta", "support_title_other_links": "Iba pang mga link sa suporta", - "sweeping_wallet": "Pagwawalis ng pitaka", - "sweeping_wallet_alert": "Hindi ito dapat magtagal. Huwag iwanan ang screen na ito o maaaring mawala ang mga pondo ng swept.", + "sweeping_wallet": "Sweeping wallet", + "sweeping_wallet_alert": "Hindi ito dapat magtagal. HUWAG iwanan ang screen na ito o maaaring mawala ang mga pondo.", "switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli", "switchToEVMCompatibleWallet": "Mangyaring lumipat sa isang EVM compatible na wallet at subukang muli (Ethereum, Polygon)", "symbol": "Simbolo", - "sync_all_wallets": "I -sync ang lahat ng mga pitaka", - "sync_status_attempting_sync": "Pagtatangka ng pag -sync", - "sync_status_connected": "Konektado", - "sync_status_connecting": "Pagkonekta", - "sync_status_failed_connect": "Naka -disconnect", + "sync_all_wallets": "I-sync ang lahat ng mga wallet", + "sync_status_attempting_sync": "SINUSUBUKANG I-SYNC", + "sync_status_connected": "KONEKTADO", + "sync_status_connecting": "KUMOKENEKTA", + "sync_status_failed_connect": "NADISKONEKTA", "sync_status_not_connected": "HINDI KONEKTADO", - "sync_status_starting_scan": "Simula sa pag -scan", - "sync_status_starting_sync": "Simula sa pag -sync", - "sync_status_syncronized": "Naka -synchronize", - "sync_status_syncronizing": "Pag -synchronize", - "sync_status_timed_out": "Nag -time out", - "sync_status_unsupported": "Hindi suportadong node", - "syncing_wallet_alert_content": "Ang iyong balanse at listahan ng transaksyon ay maaaring hindi kumpleto hanggang sa sabihin nito na \"naka -synchronize\" sa tuktok. Mag -click/tap upang malaman ang higit pa.", - "syncing_wallet_alert_title": "Ang iyong pitaka ay nag -sync", + "sync_status_starting_scan": "SIMULA SA PAG-SCAN", + "sync_status_starting_sync": "SIMULA SA PAG-SYNC", + "sync_status_syncronized": "NAKA-SYNCHRONIZE", + "sync_status_syncronizing": "PAG-SYNCHRONIZE", + "sync_status_timed_out": "NAG-TIME OUT", + "sync_status_unsupported": "HINDI SUPORTADONG NODE", + "syncing_wallet_alert_content": "Ang iyong balanse at listahan ng transaksyon ay maaaring hindi kumpleto hanggang sa sabihin nito na \"NAKA-SYNCHRONIZE\" sa tuktok. Mag-click/tap upang malaman ang higit pa.", + "syncing_wallet_alert_title": "Ang iyong wallet ay nag-sync", "template": "Template", "template_name": "Pangalan ng Template", "testnet_coins_no_value": "Ang mga barya ng testnet ay walang halaga", - "third_intro_content": "Ang mga yats ay nakatira sa labas ng cake wallet, din. Ang anumang address ng pitaka sa mundo ay maaaring mapalitan ng isang yat!", - "third_intro_title": "Si Yat ay mahusay na gumaganap sa iba", - "thorchain_contract_address_not_supported": "Hindi sinusuportahan ng Thorchain ang pagpapadala sa isang address ng kontrata", - "thorchain_taproot_address_not_supported": "Ang Tagabigay ng Thorchain ay hindi sumusuporta sa mga address ng taproot. Mangyaring baguhin ang address o pumili ng ibang provider.", + "third_intro_content": "Nabubuhay rin ang Yats sa labas ng Cake Wallet. Anumang wallet address sa mundo ay maaaring palitan ng Yat!", + "third_intro_title": "Magaling makipaglaro ang Yat sa iba", + "thorchain_contract_address_not_supported": "Hindi sinusuportahan ng THORChain ang pagpapadala sa isang address ng kontrata", + "thorchain_taproot_address_not_supported": "Ang provider ng THORChain ay hindi sumusuporta sa mga address ng Taproot. Mangyaring baguhin ang address o pumili ng ibang provider.", "time": "${minutes} m ${seconds} s", "tip": "Tip:", "today": "Ngayon", - "token_contract_address": "Token Address ng Kontrata", - "token_decimal": "Token Decimal", - "token_name": "Pangalan ng Token hal: Tether", - "token_symbol": "Simbolo ng token hal: USDT", + "token_contract_address": "Address ng token contract", + "token_decimal": "Token decimal", + "token_name": "Pangalan ng token, halimbawa: Tether", + "token_symbol": "Simbolo ng token, halimbawa: USDT", "tokenID": "ID", "tor_connection": "Koneksyon ng Tor", - "tor_only": "Tor lang", + "tor_only": "Tor lamang", "total": "Kabuuan", - "total_saving": "Kabuuang pagtitipid", - "totp_2fa_failure": "Maling code. Mangyaring subukan ang ibang code o makabuo ng isang bagong lihim na susi. Gumamit ng isang katugmang 2FA app na sumusuporta sa 8-digit na mga code at SHA512.", - "totp_2fa_success": "Tagumpay! Pinagana ang cake 2FA para sa pitaka na ito. Tandaan na i -save ang iyong mnemonic seed kung sakaling mawalan ka ng pag -access sa pitaka.", + "total_saving": "Kabuuang ipon", + "totp_2fa_failure": "Maling code. Mangyaring subukan ang ibang code o makabuo ng isang bagong secret key. Gumamit ng isang katugmang 2FA app na sumusuporta sa 8-digit na mga code at SHA512.", + "totp_2fa_success": "Tagumpay! Pinagana ang Cake 2FA para sa wallet na ito. Tandaan na i-save ang iyong mnemonic seed kung sakaling mawalan ka ng pag-access sa wallet.", "totp_auth_url": "TOTP AUTH URL", - "totp_code": "TOTP code", + "totp_code": "TOTP Code", "totp_secret_code": "TOTP Secret Code", "totp_verification_success": "Matagumpay ang pagpapatunay!", "track": "Subaybayan", @@ -738,14 +738,14 @@ "trade_details_fetching": "Pagkuha", "trade_details_id": "ID", "trade_details_pair": "Pares", - "trade_details_provider": "Tagabigay", + "trade_details_provider": "Provider", "trade_details_state": "Katayuan", - "trade_details_title": "Mga detalye sa kalakalan", + "trade_details_title": "Mga detalye ng kalakalan", "trade_for_not_created": "Ang kalakalan para sa ${title} ay hindi nilikha.", - "trade_history_title": "Kasaysayan ng Kalakal", + "trade_history_title": "Kasaysayan ng kalakalan", "trade_id": "Trade ID:", - "trade_id_not_found": "Trade ${tradeId} ng ${title} Hindi natagpuan.", - "trade_is_powered_by": "Ang kalakalan na ito ay pinalakas ng ${provider}", + "trade_id_not_found": "Kalakala na ${tradeId} ng ${title} ay hindi natagpuan.", + "trade_is_powered_by": "Ang kalakal na ito ay pinatakbo ng ${provider}", "trade_not_created": "Hindi nilikha ang kalakalan", "trade_not_found": "Hindi natagpuan ang kalakalan.", "trade_state_btc_sent": "Ipinadala ang BTC", @@ -753,134 +753,134 @@ "trade_state_confirming": "Pagkumpirma", "trade_state_created": "Nilikha", "trade_state_finished": "Tapos na", - "trade_state_paid": "Bayad", + "trade_state_paid": "Binayaran", "trade_state_paid_unconfirmed": "Bayad na hindi nakumpirma", - "trade_state_pending": "Nakabinbin", - "trade_state_timeout": "Oras ng oras", - "trade_state_to_be_created": "Upang malikha", + "trade_state_pending": "Hindi pa tapos", + "trade_state_timeout": "Timeout", + "trade_state_to_be_created": "lilikhain", "trade_state_traded": "Ipinagpalit", "trade_state_trading": "Pangangalakal", - "trade_state_underpaid": "Underpaid", - "trade_state_unpaid": "Walang bayad", - "trades": "Trading", + "trade_state_underpaid": "Kulang sa bayad", + "trade_state_unpaid": "Hindi nabayaran", + "trades": "Pangangalakal", "transaction_details_amount": "Halaga", "transaction_details_copied": "${title} kinopya sa clipboard", "transaction_details_date": "Petsa", - "transaction_details_fee": "Bayad", - "transaction_details_height": "Taas", + "transaction_details_fee": "Fee", + "transaction_details_height": "Height", "transaction_details_recipient_address": "Mga address ng tatanggap", - "transaction_details_source_address": "SOURCE ADDRESS", + "transaction_details_source_address": "Address ng pinagmulan", "transaction_details_title": "Mga detalye ng transaksyon", "transaction_details_transaction_id": "Transaction ID", - "transaction_key": "Susi ng transaksyon", + "transaction_key": "Transaction Key", "transaction_priority_fast": "Mabilis", "transaction_priority_fastest": "Pinakamabilis", - "transaction_priority_medium": "Katamtaman", + "transaction_priority_medium": "Medium", "transaction_priority_regular": "Regular", "transaction_priority_slow": "Mabagal", "transaction_sent": "Ipinadala ang transaksyon!", - "transaction_sent_notice": "Kung ang screen ay hindi magpatuloy pagkatapos ng 1 minuto, suriin ang isang block explorer at ang iyong email.", + "transaction_sent_notice": "Kung hindi magpapatuloy ang screen pagkatapos ng 1 minuto, tingnan ang block explorer at ang iyong email.", "transactions": "Mga Transaksyon", - "transactions_by_date": "Mga Transaksyon ayon sa Petsa", - "trongrid_history": "Kasaysayan ng Trongrid", + "transactions_by_date": "Mga transaksyon ayon sa petsa", + "trongrid_history": "Kasaysayan ng TronGrid", "trusted": "Pinagkakatiwalaan", - "tx_commit_exception_no_dust_on_change": "Ang transaksyon ay tinanggihan sa halagang ito. Sa mga barya na ito maaari kang magpadala ng ${min} nang walang pagbabago o ${max} na nagbabalik ng pagbabago.", - "tx_commit_failed": "Nabigo ang transaksyon sa transaksyon. Mangyaring makipag -ugnay sa suporta.", - "tx_invalid_input": "Gumagamit ka ng maling uri ng pag -input para sa ganitong uri ng pagbabayad", - "tx_no_dust_exception": "Ang transaksyon ay tinanggihan sa pamamagitan ng pagpapadala ng isang maliit na maliit. Mangyaring subukang dagdagan ang halaga.", - "tx_not_enough_inputs_exception": "Hindi sapat na magagamit ang mga input. Mangyaring pumili ng higit pa sa ilalim ng control ng barya", - "tx_rejected_bip68_final": "Ang transaksyon ay hindi nakumpirma na mga input at nabigo na palitan ng bayad.", - "tx_rejected_dust_change": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng pagbabago (alikabok). Subukang ipadala ang lahat o bawasan ang halaga.", - "tx_rejected_dust_output": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng output (alikabok). Mangyaring dagdagan ang halaga.", - "tx_rejected_dust_output_send_all": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng output (alikabok). Mangyaring suriin ang balanse ng mga barya na napili sa ilalim ng kontrol ng barya.", - "tx_rejected_vout_negative": "Hindi sapat na balanse upang magbayad para sa mga bayarin ng transaksyon na ito. Mangyaring suriin ang balanse ng mga barya sa ilalim ng kontrol ng barya.", + "tx_commit_exception_no_dust_on_change": "Ang transaksyon ay tinanggihan sa halagang ito. Sa mga barya na ito maaari kang magpadala ng ${min} nang walang sukli o ${max} na nagbabalik ng sukli.", + "tx_commit_failed": "Nabigo ang transaksyon. Mangyaring makipag-ugnay sa suporta.", + "tx_invalid_input": "Gumagamit ka ng maling uri ng pag-input para sa ganitong uri ng pagbabayad", + "tx_no_dust_exception": "Ang transaksyon ay tinanggihan sa pamamagitan ng pagpapadala ng isang maliit na halaga. Mangyaring subukang dagdagan ang halaga.", + "tx_not_enough_inputs_exception": "Hindi sapat na magagamit ang mga input. Mangyaring pumili ng higit pa sa ilalim ng Coin Control", + "tx_rejected_bip68_final": "Ang transaksyon ay hindi nakumpirma na mga input at nabigo na palitan ng fee.", + "tx_rejected_dust_change": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng fee (dust). Subukang ipadala ang lahat o bawasan ang halaga.", + "tx_rejected_dust_output": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng output (dust). Mangyaring dagdagan ang halaga.", + "tx_rejected_dust_output_send_all": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng output (dust). Mangyaring suriin ang balanse ng mga barya na napili sa ilalim ng Coin Control.", + "tx_rejected_vout_negative": "Hindi sapat na balanse upang magbayad para sa mga fee ng transaksyon na ito. Mangyaring suriin ang balanse ng mga barya sa ilalim ng Coin Control.", "tx_wrong_balance_exception": "Wala kang sapat na ${currency} upang maipadala ang halagang ito.", - "tx_wrong_balance_with_amount_exception": "Wala kang sapat ${currency} Upang ipadala ang kabuuang halaga ng ${amount}", - "tx_zero_fee_exception": "Hindi maaaring magpadala ng transaksyon na may 0 bayad. Subukan ang pagtaas ng rate o pagsuri sa iyong koneksyon para sa pinakabagong mga pagtatantya.", + "tx_wrong_balance_with_amount_exception": "Wala kang sapat ${currency} upang ipadala ang kabuuang halaga ng ${amount}", + "tx_zero_fee_exception": "Hindi maaaring magpadala ng transaksyon na may 0 fee. Subukan ang pagtaas ng rate o pagsuri sa iyong koneksyon para sa pinakabagong mga pagtatantya.", "unavailable_balance": "Hindi available na balanse", - "unavailable_balance_description": "Hindi Available na Balanse: Kasama sa kabuuang ito ang mga pondong naka-lock sa mga nakabinbing transaksyon at ang mga aktibong na-freeze mo sa iyong mga setting ng kontrol ng coin. Magiging available ang mga naka-lock na balanse kapag nakumpleto na ang kani-kanilang mga transaksyon, habang ang mga nakapirming balanse ay nananatiling hindi naa-access para sa mga transaksyon hanggang sa magpasya kang i-unfreeze ang mga ito.", + "unavailable_balance_description": "Hindi available na balanse: Kasama sa kabuuang ito ang mga pondong naka-lock sa mga nakabinbing transaksyon at ang mga aktibong na-freeze mo sa iyong mga setting ng Coin Control. Magiging available ang mga naka-lock na balanse kapag nakumpleto na ang kani-kanilang mga transaksyon, habang ang mga nakapirming balanse ay nananatiling hindi naa-access para sa mga transaksyon hanggang sa magpasya kang i-unfreeze ang mga ito.", "unconfirmed": "Hindi nakumpirma na balanse", - "understand": "naiintindihan ko", - "unlock": "I -unlock", - "unmatched_currencies": "Ang pera ng iyong kasalukuyang pitaka ay hindi tumutugma sa na -scan na QR", - "unspent_change": "Baguhin", - "unspent_coins_details_title": "Mga Detalye ng Unspent Coins", - "unspent_coins_title": "Unspent barya", - "unsupported_asset": "Hindi namin sinusuportahan ang pagkilos na ito para sa asset na ito. Mangyaring lumikha o lumipat sa isang pitaka ng isang suportadong uri ng pag -aari.", + "understand": "Naiitindihan ko", + "unlock": "I-unlock", + "unmatched_currencies": "Hindi tumutugma ang pera ng iyong kasalukuyang wallet sa na-scan na QR", + "unspent_change": "Sukli", + "unspent_coins_details_title": "Mga detalye ng mga hindi nagastos na barya", + "unspent_coins_title": "Mga hindi nagamit na barya", + "unsupported_asset": "Hindi namin sinusuportahan ang pagkilos na ito para sa asset na ito. Mangyaring lumikha o lumipat sa isang wallet ng isang suportadong uri ng asset.", "uptime": "Uptime", - "upto": "Hanggang sa ${value}", + "upto": "hanggang sa ${value}", "usb": "USB", - "use": "Lumipat sa", - "use_card_info_three": "Gamitin ang digital card online o sa mga pamamaraan ng pagbabayad na walang contact.", - "use_card_info_two": "Ang mga pondo ay na -convert sa USD kapag gaganapin sila sa prepaid account, hindi sa mga digital na pera.", + "use": "Lumipat sa ", + "use_card_info_three": "Gamitin ang digital card online o sa mga paraan ng pagbabayad na walang contact.", + "use_card_info_two": "Ang mga pondo ay na-convert sa USD kapag hawak sa prepaid account, hindi sa mga digital na pera.", "use_ssl": "Gumamit ng SSL", "use_suggested": "Gumamit ng iminungkahing", "use_testnet": "Gumamit ng testnet", "value": "Halaga", "value_type": "Uri ng halaga", - "variable_pair_not_supported": "Ang variable na pares na ito ay hindi suportado sa mga napiling palitan", - "verification": "Pag -verify", - "verify_with_2fa": "Mag -verify sa cake 2FA", + "variable_pair_not_supported": "Ang variable na pares na ito ay hindi suportado sa mga napiling exchange", + "verification": "Pag-verify", + "verify_with_2fa": "Mag-verify sa Cake 2FA", "version": "Bersyon ${currentVersion}", "view_all": "Tingnan lahat", "view_in_block_explorer": "Tingnan sa Block Explorer", - "view_key_private": "Tingnan ang Key (Pribado)", - "view_key_public": "Tingnan ang Key (Publiko)", - "view_transaction_on": "Tingnan ang transaksyon sa", + "view_key_private": "Tingnan ang view key (private)", + "view_key_public": "Tingnan ang view key (public)", + "view_transaction_on": "Tingnan ang transaksyon sa ", "voting_weight": "Bigat ng pagboto", "waitFewSecondForTxUpdate": "Mangyaring maghintay ng ilang segundo para makita ang transaksyon sa history ng mga transaksyon", - "wallet_keys": "Mga buto/susi ng pitaka", - "wallet_list_create_new_wallet": "Lumikha ng bagong pitaka", - "wallet_list_edit_wallet": "I -edit ang Wallet", - "wallet_list_failed_to_load": "Nabigong mag -load ng ${wallet_name} pitaka. ${error}", + "wallet_keys": "Wallet seed/keys", + "wallet_list_create_new_wallet": "Lumikha ng bagong wallet", + "wallet_list_edit_wallet": "I-edit ang wallet", + "wallet_list_failed_to_load": "Nabigong na-load ang ${wallet_name} na wallet. ${error}", "wallet_list_failed_to_remove": "Nabigong alisin ang ${wallet_name} wallet. ${error}", - "wallet_list_load_wallet": "Mag -load ng pitaka", - "wallet_list_loading_wallet": "Naglo -load ng ${wallet_name} Wallet", - "wallet_list_removing_wallet": "Pag -alis ng ${wallet_name} Wallet", - "wallet_list_restore_wallet": "Ibalik ang pitaka", + "wallet_list_load_wallet": "I-load ang wallet", + "wallet_list_loading_wallet": "Naglo-load ng ${wallet_name} wallet", + "wallet_list_removing_wallet": "Nag-aalis ang ${wallet_name} na wallet", + "wallet_list_restore_wallet": "Ibalik ang Wallet", "wallet_list_title": "Monero Wallet", - "wallet_list_wallet_name": "Pangalan ng Wallet", + "wallet_list_wallet_name": "Pangalan ng wallet", "wallet_menu": "Menu", - "wallet_name": "Pangalan ng Wallet", - "wallet_name_exists": "Ang isang pitaka na may pangalang iyon ay mayroon na. Mangyaring pumili ng ibang pangalan o palitan muna ang iba pang pitaka.", + "wallet_name": "Pangalan ng wallet", + "wallet_name_exists": "Ang isang wallet na may pangalang iyon ay mayroon na. Mangyaring pumili ng ibang pangalan o palitan muna ang iba pang wallet.", "wallet_password_is_empty": "Walang laman ang password ng wallet. Ang password ng wallet ay hindi dapat walang laman", - "wallet_recovery_height": "Taas ng pagbawi", - "wallet_restoration_store_incorrect_seed_length": "Maling haba ng binhi", - "wallet_seed": "SEED ng Wallet", + "wallet_recovery_height": "Recovery Height", + "wallet_restoration_store_incorrect_seed_length": "Maling haba ng seed", + "wallet_seed": "Wallet seed", "wallet_seed_legacy": "Legacy wallet seed", "wallet_store_monero_wallet": "Monero Wallet", "walletConnect": "WalletConnect", - "wallets": "Wallets", + "wallets": "Mga Wallet", "warning": "Babala", "welcome": "Maligayang pagdating sa", - "welcome_to_cakepay": "Maligayang pagdating sa cake pay!", + "welcome_to_cakepay": "Maligayang pagdating sa Cake Pay!", "what_is_silent_payments": "Ano ang tahimik na pagbabayad?", "widgets_address": "Address", "widgets_or": "o", - "widgets_restore_from_blockheight": "Ibalik mula sa blockheight", + "widgets_restore_from_blockheight": "Ibalik mula sa block height", "widgets_restore_from_date": "Ibalik mula sa petsa", - "widgets_seed": "Binhi", + "widgets_seed": "Seed", "wouoldLikeToConnect": "gustong kumonekta", - "write_down_backup_password": "Mangyaring isulat ang iyong backup password, na ginagamit para sa pag -import ng iyong mga backup file.", - "xlm_extra_info": "Mangyaring huwag kalimutan na tukuyin ang memo ID habang ipinapadala ang transaksyon ng XLM para sa palitan", - "xmr_available_balance": "Magagamit na balanse", - "xmr_full_balance": "Buong balanse", + "write_down_backup_password": "Mangyaring isulat ang iyong backup na password na ginagamit para sa pag-import ng iyong mga backup na file.", + "xlm_extra_info": "Mangyaring huwag kalimutang tukuyin ang Memo ID habang ipinapadala ang transaksyon sa XLM para sa palitan", + "xmr_available_balance": "Magagamit na Balanse", + "xmr_full_balance": "Buong Balanse", "xmr_hidden": "Nakatago", - "xmr_to_error": "Xmr.to error", - "xmr_to_error_description": "Di -wastong halaga. Pinakamataas na limitasyon ng 8 numero pagkatapos ng punto ng desimal", - "xrp_extra_info": "Mangyaring huwag kalimutan na tukuyin ang patutunguhan na tag habang ipinapadala ang transaksyon ng XRP para sa palitan", + "xmr_to_error": "XMR.TO error", + "xmr_to_error_description": "Hindi wastong halaga. Maximum�na limitasyon 8 digit pagkatapos ng decimal point", + "xrp_extra_info": "Mangyaring huwag kalimutan na tukuyin ang Destination Tag habang ipinapadala ang transaksyon ng XRP para sa palitan", "yat": "Yat", - "yat_address": "Yat address", - "yat_alert_content": "Ang mga gumagamit ng cake wallet ay maaari na ngayong magpadala at makatanggap ng lahat ng kanilang mga paboritong pera na may isang one-of-a-kind emoji-based username.", - "yat_alert_title": "Magpadala at tumanggap ng crypto nang mas madali sa yat", - "yat_error": "Error sa yat", - "yat_error_content": "Walang mga address na naka -link sa yat na ito. Subukan ang isa pang yat", - "yat_popup_content": "Maaari ka na ngayong magpadala at makatanggap ng crypto sa cake wallet kasama ang iyong yat - isang maikli, emoji na batay sa username. Pamahalaan ang mga yats anumang oras sa screen ng Mga Setting", - "yat_popup_title": "Ang iyong wallet address ay maaaring ma -emojified.", + "yat_address": "Yat Address", + "yat_alert_content": "Ang mga gumagamit ng Cake Wallet ay maaari na ngayong magpadala at tumanggap ng lahat ng kanilang mga paboritong pera gamit ang isa sa isang uri ng emoji-based na username.", + "yat_alert_title": "Magpadala at tumanggap ng crypto nang mas madali gamit ang Yat", + "yat_error": "Error sa Yat", + "yat_error_content": "Walang mga address na naka-link sa Yat na ito. Subukan ang isa pang Yat", + "yat_popup_content": "Maaari ka na ngayong magpadala at tumanggap ng crypto sa Cake Wallet gamit ang iyong Yat - isang maikling emoji-based na username. Pamahalaan ang Yats anumang oras sa screen ng mga setting", + "yat_popup_title": "Ang iyong wallet address ay maaring ma-emojified.", "yesterday": "Kahapon", "you_now_have_debit_card": "Mayroon ka na ngayong debit card", - "you_pay": "Magbabayad ka", - "you_will_get": "Mag -convert sa", - "you_will_send": "I -convert mula sa", + "you_pay": "Magbayad ka", + "you_will_get": "I-convert sa", + "you_will_send": "I-convert mula sa", "yy": "YY" -} \ No newline at end of file +} From 102139b0611dc3dceffedea26085eaef89ac31f4 Mon Sep 17 00:00:00 2001 From: Rafael Date: Thu, 15 Aug 2024 22:34:41 -0300 Subject: [PATCH 035/142] feat: more info on privkey error (#1612) --- cw_bitcoin/lib/electrum_wallet.dart | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 501d94e54..9dc8de083 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -993,11 +993,29 @@ abstract class ElectrumWalletBase bool hasTaprootInputs = false; final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { - final key = estimatedTx.inputPrivKeyInfos - .firstWhereOrNull((element) => element.privkey.getPublic().toHex() == publicKey); + String error = "Cannot find private key."; + + ECPrivateInfo? key; + + if (estimatedTx.inputPrivKeyInfos.isEmpty) { + error += "\nNo private keys generated."; + } else { + error += "\nAddress: ${utxo.ownerDetails.address.toAddress()}"; + + key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) { + final elemPubkey = element.privkey.getPublic().toHex(); + if (elemPubkey == publicKey) { + return true; + } else { + error += "\nExpected: $publicKey"; + error += "\nPubkey: $elemPubkey"; + return false; + } + }); + } if (key == null) { - throw Exception("Cannot find private key"); + throw Exception(error); } if (utxo.utxo.isP2tr()) { From fb3f64facfb2dd5b6eb1b236a8c1fc074384f3ab Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 16 Aug 2024 23:21:03 +0100 Subject: [PATCH 036/142] fix: Restore backup bug fix and clean up sol error message (#1614) * fix: Restore backup bug fix and clean up sol error message * fix: Add async await to reinitialize --- lib/reactions/bootstrap.dart | 10 ++++---- .../on_authentication_state_change.dart | 16 +++++++++++-- lib/view_model/send/send_view_model.dart | 24 +++++++++++++++++++ res/values/strings_ar.arb | 2 ++ res/values/strings_bg.arb | 2 ++ res/values/strings_cs.arb | 2 ++ res/values/strings_de.arb | 2 ++ res/values/strings_en.arb | 2 ++ res/values/strings_es.arb | 2 ++ res/values/strings_fr.arb | 2 ++ res/values/strings_ha.arb | 2 ++ res/values/strings_hi.arb | 2 ++ res/values/strings_hr.arb | 2 ++ res/values/strings_id.arb | 2 ++ res/values/strings_it.arb | 2 ++ res/values/strings_ja.arb | 2 ++ res/values/strings_ko.arb | 2 ++ res/values/strings_my.arb | 2 ++ res/values/strings_nl.arb | 2 ++ res/values/strings_pl.arb | 2 ++ res/values/strings_pt.arb | 2 ++ res/values/strings_ru.arb | 2 ++ res/values/strings_th.arb | 2 ++ res/values/strings_tl.arb | 4 +++- res/values/strings_tr.arb | 2 ++ res/values/strings_uk.arb | 2 ++ res/values/strings_ur.arb | 2 ++ res/values/strings_yo.arb | 2 ++ res/values/strings_zh.arb | 2 ++ 29 files changed, 95 insertions(+), 9 deletions(-) diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index e78d8a01d..5b1a0ace7 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -21,16 +21,14 @@ Future bootstrap(GlobalKey navigatorKey) async { final settingsStore = getIt.get(); final fiatConversionStore = getIt.get(); - final currentWalletName = getIt - .get() - .getString(PreferencesKey.currentWalletName); + final currentWalletName = + getIt.get().getString(PreferencesKey.currentWalletName); if (currentWalletName != null) { authenticationStore.installed(); } - startAuthenticationStateChange(authenticationStore, navigatorKey); - startCurrentWalletChangeReaction( - appStore, settingsStore, fiatConversionStore); + await startAuthenticationStateChange(authenticationStore, navigatorKey); + startCurrentWalletChangeReaction(appStore, settingsStore, fiatConversionStore); startCurrentFiatChangeReaction(appStore, settingsStore, fiatConversionStore); startCurrentFiatApiModeChangeReaction(appStore, settingsStore, fiatConversionStore); startOnCurrentNodeChangeReaction(appStore); diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 95cbd51df..014306b98 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -13,8 +13,20 @@ ReactionDisposer? _onAuthenticationStateChange; dynamic loginError; StreamController authenticatedErrorStreamController = StreamController(); -void startAuthenticationStateChange( - AuthenticationStore authenticationStore, GlobalKey navigatorKey) { +Future reInitializeStreamController() async { + if (!authenticatedErrorStreamController.isClosed) { + await authenticatedErrorStreamController.close(); + } + + authenticatedErrorStreamController = StreamController(); +} + +Future startAuthenticationStateChange( + AuthenticationStore authenticationStore, + GlobalKey navigatorKey, +) async { + await reInitializeStreamController(); + authenticatedErrorStreamController.stream.listen((event) { if (authenticationStore.state == AuthenticationState.allowed) { ExceptionHandler.showError(event.toString(), delayInSeconds: 3); diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index d0514bb19..863c83957 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -583,6 +583,30 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor String errorMessage = error.toString(); if (walletType == WalletType.solana) { + if (errorMessage.contains('insufficient lamports')) { + double solValueNeeded = 0.0; + + // Regular expression to match the number after "need". This shows the exact lamports the user needs to perform the transaction. + RegExp regExp = RegExp(r'need (\d+)'); + + // Find the match + Match? match = regExp.firstMatch(errorMessage); + + if (match != null) { + String neededAmount = match.group(1)!; + final lamportsNeeded = int.tryParse(neededAmount); + + // 5000 lamport used here is the constant for sending a transaction on solana + int lamportsPerSol = 1000000000; + + solValueNeeded = + lamportsNeeded != null ? ((lamportsNeeded + 5000) / lamportsPerSol) : 0.0; + return S.current.insufficient_lamports(solValueNeeded.toString()); + } else { + print("No match found."); + return S.current.insufficient_lamport_for_tx; + } + } if (errorMessage.contains('insufficient funds for rent')) { return S.current.insufficientFundsForRentError; } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 98cafdebc..a73067383 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -338,6 +338,8 @@ "incoming": "الواردة", "incorrect_seed": "النص الذي تم إدخاله غير صالح.", "inputs": "المدخلات", + "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": "مدخل غير صالح", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index b51167dab..b60cbc55c 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -338,6 +338,8 @@ "incoming": "Входящи", "incorrect_seed": "Въведеният текст е невалиден.", "inputs": "Входове", + "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": "Невалиден вход", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index d47ca932c..537695dd9 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -338,6 +338,8 @@ "incoming": "Příchozí", "incorrect_seed": "Zadaný text není správný.", "inputs": "Vstupy", + "insufficient_lamport_for_tx": "Nemáte dostatek SOL na pokrytí transakce a jejího transakčního poplatku. Laskavě přidejte do své peněženky více solu nebo snižte množství Sol, kterou odesíláte.", + "insufficient_lamports": "Nemáte dostatek SOL na pokrytí transakce a jejího transakčního poplatku. Potřebujete alespoň ${solValueNeeded} sol. Laskavě přidejte do své peněženky více SOL nebo snižte množství Sol, kterou odesíláte", "insufficientFundsForRentError": "Nemáte dostatek SOL na pokrytí transakčního poplatku a nájemného za účet. Laskavě přidejte do své peněženky více SOL nebo snižte množství Sol, kterou odesíláte", "introducing_cake_pay": "Představujeme Cake Pay!", "invalid_input": "Neplatný vstup", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index e64c3bc27..052015367 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -338,6 +338,8 @@ "incoming": "Eingehend", "incorrect_seed": "Der eingegebene Text ist ungültig.", "inputs": "Eingänge", + "insufficient_lamport_for_tx": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Bitte fügen Sie Ihrer Brieftasche mehr Sol hinzu oder reduzieren Sie die SO -Menge, die Sie senden.", + "insufficient_lamports": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Sie brauchen mindestens ${solValueNeeded} Sol. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Brieftasche hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", "introducing_cake_pay": "Einführung von Cake Pay!", "invalid_input": "Ungültige Eingabe", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 9d7d9ad49..0f0ebd470 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -338,6 +338,8 @@ "incoming": "Incoming", "incorrect_seed": "The text entered is not valid.", "inputs": "Inputs", + "insufficient_lamport_for_tx": "You do not have enough SOL to cover the transaction and its transaction fee. Kindly add more SOL to your wallet or reduce the SOL amount you\\'re sending.", + "insufficient_lamports": "You do not have enough SOL to cover the transaction and its transaction fee. You need at least ${solValueNeeded} SOL. Kindly add more SOL to your wallet or reduce the SOL amount you\\'re sending", "insufficientFundsForRentError": "You do not have enough SOL to cover the transaction fee and rent for the account. Kindly add more SOL to your wallet or reduce the SOL amount you\\'re sending", "introducing_cake_pay": "Introducing Cake Pay!", "invalid_input": "Invalid input", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index e7f1366a5..1e6eeae59 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -338,6 +338,8 @@ "incoming": "Entrante", "incorrect_seed": "El texto ingresado no es válido.", "inputs": "Entradas", + "insufficient_lamport_for_tx": "No tiene suficiente SOL para cubrir la transacción y su tarifa de transacción. Por favor, agregue más SOL a su billetera o reduzca la cantidad de sol que está enviando.", + "insufficient_lamports": "No tiene suficiente SOL para cubrir la transacción y su tarifa de transacción. Necesita al menos ${solValueNeeded} sol. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando", "insufficientFundsForRentError": "No tiene suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando", "introducing_cake_pay": "¡Presentamos Cake Pay!", "invalid_input": "Entrada inválida", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 7965eca12..a64a69514 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -338,6 +338,8 @@ "incoming": "Entrantes", "incorrect_seed": "Le texte entré est invalide.", "inputs": "Contributions", + "insufficient_lamport_for_tx": "Vous n'avez pas assez de sol pour couvrir la transaction et ses frais de transaction. Veuillez ajouter plus de Sol à votre portefeuille ou réduire la quantité de Sol que vous envoyez.", + "insufficient_lamports": "Vous n'avez pas assez de sol pour couvrir la transaction et ses frais de transaction. Vous avez besoin d'au moins ${solValueNeeded} sol. Veuillez ajouter plus de Sol à votre portefeuille ou réduire la quantité de sol que vous envoyez", "insufficientFundsForRentError": "Vous n'avez pas assez de SOL pour couvrir les frais de transaction et le loyer pour le compte. Veuillez ajouter plus de Sol à votre portefeuille ou réduire la quantité de sol que vous envoyez", "introducing_cake_pay": "Présentation de Cake Pay !", "invalid_input": "Entrée invalide", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index d2c58971f..219b3c939 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -338,6 +338,8 @@ "incoming": "Mai shigowa", "incorrect_seed": "rubutun da aka shigar ba shi da inganci.", "inputs": "Abubuwan da ke ciki", + "insufficient_lamport_for_tx": "Ba ku da isasshen sool don rufe ma'amala da kuɗin ma'amala. Da unara ƙara ƙarin sool a cikin walat ɗinku ko rage adadin Sol ɗin da kuke aikawa.", + "insufficient_lamports": "Ba ku da isasshen sool don rufe ma'amala da kuɗin ma'amala. Kuna buƙatar aƙalla ${solValueNeeded} Sol. Da kyau ƙara ƙarin sool zuwa walat ɗinku ko rage adadin Sol ɗin da kuke aikawa", "insufficientFundsForRentError": "Ba ku da isasshen Sol don rufe kuɗin ma'amala da haya don asusun. Da kyau ƙara ƙarin sool zuwa walat ɗinku ko rage adadin Sol ɗin da kuke aikawa", "introducing_cake_pay": "Gabatar da Cake Pay!", "invalid_input": "Shigar da ba daidai ba", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index baa3925c0..b711939a4 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -338,6 +338,8 @@ "incoming": "आने वाली", "incorrect_seed": "दर्ज किया गया पाठ मान्य नहीं है।", "inputs": "इनपुट", + "insufficient_lamport_for_tx": "आपके पास लेनदेन और इसके लेनदेन शुल्क को कवर करने के लिए पर्याप्त सोल नहीं है। कृपया अपने बटुए में अधिक सोल जोड़ें या आपके द्वारा भेजे जा रहे सोल राशि को कम करें।", + "insufficient_lamports": "आपके पास लेनदेन और इसके लेनदेन शुल्क को कवर करने के लिए पर्याप्त सोल नहीं है। आपको कम से कम ${solValueNeeded} सोल की आवश्यकता है। कृपया अपने बटुए में अधिक सोल जोड़ें या सोल राशि को कम करें जिसे आप भेज रहे हैं", "insufficientFundsForRentError": "आपके पास लेन -देन शुल्क और खाते के लिए किराए को कवर करने के लिए पर्याप्त सोल नहीं है। कृपया अपने बटुए में अधिक सोल जोड़ें या सोल राशि को कम करें जिसे आप भेज रहे हैं", "introducing_cake_pay": "परिचय Cake Pay!", "invalid_input": "अमान्य निवेश", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index e6b820300..401514735 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -338,6 +338,8 @@ "incoming": "Dolazno", "incorrect_seed": "Uneseni tekst nije valjan.", "inputs": "Unosi", + "insufficient_lamport_for_tx": "Nemate dovoljno SOL -a da pokriva transakciju i njegovu transakcijsku naknadu. Ljubazno dodajte više sol u svoj novčanik ili smanjite količinu SOL -a koju šaljete.", + "insufficient_lamports": "Nemate dovoljno SOL -a da pokriva transakciju i njegovu transakcijsku naknadu. Trebate najmanje ${solValueNeeded} sol. Ljubazno dodajte više sol u svoj novčanik ili smanjite količinu SOL -a koju šaljete", "insufficientFundsForRentError": "Nemate dovoljno SOL -a za pokrivanje naknade za transakciju i najamninu za račun. Ljubazno dodajte više sol u svoj novčanik ili smanjite količinu SOL -a koju šaljete", "introducing_cake_pay": "Predstavljamo Cake Pay!", "invalid_input": "Pogrešan unos", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 0b8077807..b6210b5dd 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -338,6 +338,8 @@ "incoming": "Masuk", "incorrect_seed": "Teks yang dimasukkan tidak valid.", "inputs": "Input", + "insufficient_lamport_for_tx": "Anda tidak memiliki cukup SOL untuk menutupi transaksi dan biaya transaksinya. Mohon tambahkan lebih banyak sol ke dompet Anda atau kurangi jumlah sol yang Anda kirim.", + "insufficient_lamports": "Anda tidak memiliki cukup SOL untuk menutupi transaksi dan biaya transaksinya. Anda membutuhkan setidaknya ${solValueNeeded} sol. Mohon tambahkan lebih banyak sol ke dompet Anda atau kurangi jumlah sol yang Anda kirim", "insufficientFundsForRentError": "Anda tidak memiliki cukup SOL untuk menutupi biaya transaksi dan menyewa untuk akun tersebut. Mohon tambahkan lebih banyak sol ke dompet Anda atau kurangi jumlah sol yang Anda kirim", "introducing_cake_pay": "Perkenalkan Cake Pay!", "invalid_input": "Masukan tidak valid", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index b7a81ef9a..fc4abf7c4 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -339,6 +339,8 @@ "incoming": "In arrivo", "incorrect_seed": "Il testo inserito non è valido.", "inputs": "Input", + "insufficient_lamport_for_tx": "Non hai abbastanza SOL per coprire la transazione e la sua quota di transazione. Si prega di aggiungere più SOL al tuo portafoglio o ridurre l'importo SOL che stai inviando.", + "insufficient_lamports": "Non hai abbastanza SOL per coprire la transazione e la sua quota di transazione. Hai bisogno di almeno ${solValueNeeded} sol. Si prega di aggiungere più SOL al tuo portafoglio o ridurre l'importo SOL che stai inviando", "insufficientFundsForRentError": "Non hai abbastanza SOL per coprire la tassa di transazione e l'affitto per il conto. Si prega di aggiungere più SOL al tuo portafoglio o ridurre l'importo SOL che stai inviando", "introducing_cake_pay": "Presentazione di Cake Pay!", "invalid_input": "Inserimento non valido", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index a13a4e6f0..98495fc8b 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -339,6 +339,8 @@ "incoming": "着信", "incorrect_seed": "入力されたテキストは無効です。", "inputs": "入力", + "insufficient_lamport_for_tx": "トランザクションとその取引手数料をカバーするのに十分なSOLがありません。財布にソルを追加するか、送信するソル量を減らしてください。", + "insufficient_lamports": "トランザクションとその取引手数料をカバーするのに十分なSOLがありません。少なくとも${solValueNeeded} solが必要です。財布にソルを追加するか、送信するソル量を減らしてください", "insufficientFundsForRentError": "アカウントの取引料金とレンタルをカバーするのに十分なソルがありません。財布にソルを追加するか、送信するソル量を減らしてください", "introducing_cake_pay": "序章Cake Pay!", "invalid_input": "無効入力", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index d20546f41..60c52b21f 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -338,6 +338,8 @@ "incoming": "들어오는", "incorrect_seed": "입력하신 텍스트가 유효하지 않습니다.", "inputs": "입력", + "insufficient_lamport_for_tx": "거래 및 거래 수수료를 충당하기에 충분한 SOL이 없습니다. 지갑에 더 많은 솔을 추가하거나 보내는 솔을 줄입니다.", + "insufficient_lamports": "거래 및 거래 수수료를 충당하기에 충분한 SOL이 없습니다. 최소 ${solValueNeeded} sol이 필요합니다. 지갑에 더 많은 솔을 추가하거나 보내는 솔을 줄이십시오.", "insufficientFundsForRentError": "거래 수수료와 계좌 임대료를 충당하기에 충분한 SOL이 없습니다. 지갑에 더 많은 솔을 추가하거나 보내는 솔을 줄이십시오.", "introducing_cake_pay": "소개 Cake Pay!", "invalid_input": "잘못된 입력", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 06d7cf627..42643be48 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -338,6 +338,8 @@ "incoming": "ဝင်လာ", "incorrect_seed": "ထည့်သွင်းထားသော စာသားသည် မမှန်ကန်ပါ။", "inputs": "သွင်းငေှ", + "insufficient_lamport_for_tx": "သငျသညျငွေပေးငွေယူနှင့်၎င်း၏ငွေပေးငွေယူကြေးကိုဖုံးလွှမ်းရန် sol ရှိသည်မဟုတ်ကြဘူး။ ကြင်နာစွာသင်၏ပိုက်ဆံအိတ်သို့ပိုမို sol ကိုထပ်ထည့်ပါသို့မဟုတ်သင်ပို့လွှတ်ခြင်း sol ပမာဏကိုလျှော့ချပါ။", + "insufficient_lamports": "သငျသညျငွေပေးငွေယူနှင့်၎င်း၏ငွေပေးငွေယူကြေးကိုဖုံးလွှမ်းရန် sol ရှိသည်မဟုတ်ကြဘူး။ သင်အနည်းဆုံး ${solValueNeeded} s ကိုလိုအပ်ပါတယ်။ ကြင်နာစွာသင်၏ပိုက်ဆံအိတ်သို့ပိုမို sol ကိုထပ်ထည့်ပါသို့မဟုတ်သင်ပို့နေသော sol ပမာဏကိုလျှော့ချပါ", "insufficientFundsForRentError": "သင်ငွေပေးချေမှုအခကြေးငွေကိုဖုံးအုပ်ရန်နှင့်အကောင့်ငှားရန်လုံလောက်သော sol ရှိသည်မဟုတ်ကြဘူး။ ကြင်နာစွာသင်၏ပိုက်ဆံအိတ်သို့ပိုမို sol ကိုပိုမိုထည့်ပါသို့မဟုတ်သင်ပို့ခြင်း sol ပမာဏကိုလျှော့ချပါ", "introducing_cake_pay": "Cake Pay ကို မိတ်ဆက်ခြင်း။", "invalid_input": "ထည့်သွင်းမှု မမှန်ကန်ပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 78caef912..0f6149182 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -338,6 +338,8 @@ "incoming": "inkomend", "incorrect_seed": "De ingevoerde tekst is niet geldig.", "inputs": "Invoer", + "insufficient_lamport_for_tx": "U hebt niet genoeg SOL om de transactie en de transactiekosten te dekken. Voeg vriendelijk meer SOL toe aan uw portemonnee of verminder de SOL -hoeveelheid die u verzendt.", + "insufficient_lamports": "U hebt niet genoeg SOL om de transactie en de transactiekosten te dekken. Je hebt minstens ${solValueNeeded} sol nodig. Voeg vriendelijk meer Sol toe aan uw portemonnee of verminder de SOL -hoeveelheid die u verzendt", "insufficientFundsForRentError": "U hebt niet genoeg SOL om de transactiekosten en huur voor de rekening te dekken. Voeg vriendelijk meer SOL toe aan uw portemonnee of verminder de SOL -hoeveelheid die u verzendt", "introducing_cake_pay": "Introductie van Cake Pay!", "invalid_input": "Ongeldige invoer", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 7de435319..48d6e38f6 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -338,6 +338,8 @@ "incoming": "Przychodzące", "incorrect_seed": "Wprowadzony seed jest nieprawidłowy.", "inputs": "Wejścia", + "insufficient_lamport_for_tx": "Nie masz wystarczającej ilości SOL, aby pokryć transakcję i opłatę za transakcję. Uprzejmie dodaj więcej sol do portfela lub zmniejsz wysyłaną kwotę SOL.", + "insufficient_lamports": "Nie masz wystarczającej ilości SOL, aby pokryć transakcję i opłatę za transakcję. Potrzebujesz przynajmniej ${solValueNeeded} sol. Uprzejmie dodaj więcej sol do portfela lub zmniejsz wysyłaną kwotę SOL, którą wysyłasz", "insufficientFundsForRentError": "Nie masz wystarczającej ilości SOL, aby pokryć opłatę za transakcję i czynsz za konto. Uprzejmie dodaj więcej sol do portfela lub zmniejsz solę, którą wysyłasz", "introducing_cake_pay": "Przedstawiamy Cake Pay!", "invalid_input": "Nieprawidłowe dane wejściowe", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index a3d789cab..070f3f776 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -338,6 +338,8 @@ "incoming": "Recebidas", "incorrect_seed": "O texto digitado não é válido.", "inputs": "Entradas", + "insufficient_lamport_for_tx": "Você não tem Sol suficiente para cobrir a transação e sua taxa de transação. Por favor, adicione mais sol à sua carteira ou reduza a quantidade de sol que você envia.", + "insufficient_lamports": "Você não tem Sol suficiente para cobrir a transação e sua taxa de transação. Você precisa de pelo menos ${solValueNeeded} sol. Por favor, adicione mais sol à sua carteira ou reduza a quantidade de sol que você está enviando", "insufficientFundsForRentError": "Você não tem Sol suficiente para cobrir a taxa de transação e o aluguel da conta. Por favor, adicione mais sol à sua carteira ou reduza a quantidade de sol que você envia", "introducing_cake_pay": "Apresentando o Cake Pay!", "invalid_input": "Entrada inválida", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index d84aa146f..695877c77 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -338,6 +338,8 @@ "incoming": "Входящие", "incorrect_seed": "Введённый текст некорректный.", "inputs": "Входы", + "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": "Неверный Ввод", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 2adccb2cf..5757eed0b 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -338,6 +338,8 @@ "incoming": "ขาเข้า", "incorrect_seed": "ข้อความที่ป้อนไม่ถูกต้อง", "inputs": "อินพุต", + "insufficient_lamport_for_tx": "คุณไม่มีโซลเพียงพอที่จะครอบคลุมการทำธุรกรรมและค่าธรรมเนียมการทำธุรกรรม กรุณาเพิ่มโซลให้มากขึ้นลงในกระเป๋าเงินของคุณหรือลดจำนวนโซลที่คุณส่งมา", + "insufficient_lamports": "คุณไม่มีโซลเพียงพอที่จะครอบคลุมการทำธุรกรรมและค่าธรรมเนียมการทำธุรกรรม คุณต้องการอย่างน้อย ${solValueNeeded} SOL กรุณาเพิ่มโซลให้มากขึ้นลงในกระเป๋าเงินของคุณหรือลดจำนวนโซลที่คุณกำลังส่ง", "insufficientFundsForRentError": "คุณไม่มีโซลเพียงพอที่จะครอบคลุมค่าธรรมเนียมการทำธุรกรรมและค่าเช่าสำหรับบัญชี กรุณาเพิ่มโซลให้มากขึ้นลงในกระเป๋าเงินของคุณหรือลดจำนวนโซลที่คุณส่งมา", "introducing_cake_pay": "ยินดีต้อนรับสู่ Cake Pay!", "invalid_input": "อินพุตไม่ถูกต้อง", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 22093f772..7fc83afd8 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -338,6 +338,8 @@ "incoming": "Papasok", "incorrect_seed": "Ang text na ipinasok ay hindi wasto.", "inputs": "Mga input", + "insufficient_lamport_for_tx": "Wala kang sapat na SOL upang masakop ang transaksyon at ang bayad sa transaksyon nito. Mabuting magdagdag ng higit pa sa iyong pitaka o bawasan ang sol na halaga na iyong ipinapadala.", + "insufficient_lamports": "Wala kang sapat na SOL upang masakop ang transaksyon at ang bayad sa transaksyon nito. Kailangan mo ng hindi bababa sa ${solValueNeeded} sol. Mabait na magdagdag ng higit pang sol sa iyong pitaka o bawasan ang dami ng iyong ipinapadala", "insufficientFundsForRentError": "Wala kang sapat na SOL upang masakop ang fee sa transaksyon at upa para sa account. Mabait na magdagdag ng higit pa sa iyong wallet o bawasan ang halaga ng SOL na iyong ipinapadala", "introducing_cake_pay": "Pagpapakilala ng Cake Pay!", "invalid_input": "Di-wastong input", @@ -883,4 +885,4 @@ "you_will_get": "I-convert sa", "you_will_send": "I-convert mula sa", "yy": "YY" -} +} \ No newline at end of file diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index b47787bbd..4d1aa43e4 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -338,6 +338,8 @@ "incoming": "Gelen", "incorrect_seed": "Girilen metin geçerli değil.", "inputs": "Girişler", + "insufficient_lamport_for_tx": "İşlemi ve işlem ücretini karşılamak için yeterli SOL'unuz yok. Lütfen cüzdanınıza daha fazla SOL ekleyin veya gönderdiğiniz sol miktarını azaltın.", + "insufficient_lamports": "İşlemi ve işlem ücretini karşılamak için yeterli SOL'unuz yok. En az ${solValueNeeded} Sol'a ihtiyacınız var. Lütfen cüzdanınıza daha fazla sol ekleyin veya gönderdiğiniz sol miktarını azaltın", "insufficientFundsForRentError": "İşlem ücretini karşılamak ve hesap için kiralamak için yeterli SOL'nuz yok. Lütfen cüzdanınıza daha fazla sol ekleyin veya gönderdiğiniz sol miktarını azaltın", "introducing_cake_pay": "Cake Pay ile tanışın!", "invalid_input": "Geçersiz Giriş", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 328548087..22edec0d5 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -338,6 +338,8 @@ "incoming": "Вхідні", "incorrect_seed": "Введений текст невірний.", "inputs": "Вхoди", + "insufficient_lamport_for_tx": "У вас недостатньо SOL, щоб покрити транзакцію та її плату за трансакцію. Будь ласка, додайте до свого гаманця більше SOL або зменшіть суму, яку ви надсилаєте.", + "insufficient_lamports": "У вас недостатньо SOL, щоб покрити транзакцію та її плату за трансакцію. Вам потрібно щонайменше ${solValueNeeded} sol. Будь ласка, додайте до свого гаманця більше SOL або зменшіть суму Sol, яку ви надсилаєте", "insufficientFundsForRentError": "У вас недостатньо SOL, щоб покрити плату за транзакцію та оренду на рахунок. Будь ласка, додайте до свого гаманця більше SOL або зменшіть суму, яку ви надсилаєте", "introducing_cake_pay": "Представляємо Cake Pay!", "invalid_input": "Неправильні дані", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 7d794f9bb..590f344d8 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -338,6 +338,8 @@ "incoming": "آنے والا", "incorrect_seed": "درج کردہ متن درست نہیں ہے۔", "inputs": "آدانوں", + "insufficient_lamport_for_tx": "آپ کے پاس ٹرانزیکشن اور اس کے لین دین کی فیس کا احاطہ کرنے کے لئے کافی SOL نہیں ہے۔ برائے مہربانی اپنے بٹوے میں مزید سول شامل کریں یا آپ کو بھیجنے والی سول رقم کو کم کریں۔", + "insufficient_lamports": "آپ کے پاس ٹرانزیکشن اور اس کے لین دین کی فیس کا احاطہ کرنے کے لئے کافی SOL نہیں ہے۔ آپ کو کم از کم ${solValueNeeded} sol کی ضرورت ہے۔ برائے مہربانی اپنے بٹوے میں مزید SOL شامل کریں یا آپ جس SOL رقم کو بھیج رہے ہو اسے کم کریں", "insufficientFundsForRentError": "آپ کے پاس ٹرانزیکشن فیس اور اکاؤنٹ کے لئے کرایہ لینے کے ل enough اتنا SOL نہیں ہے۔ برائے مہربانی اپنے بٹوے میں مزید سول شامل کریں یا آپ کو بھیجنے والی سول رقم کو کم کریں", "introducing_cake_pay": "Cake پے کا تعارف!", "invalid_input": "غلط ان پٹ", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 2150a503f..e5cf3d3f9 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -339,6 +339,8 @@ "incoming": "Wọ́n tó ń bọ̀", "incorrect_seed": "Ọ̀rọ̀ tí a tẹ̀ kì í ṣe èyí.", "inputs": "Igbewọle", + "insufficient_lamport_for_tx": "O ko ni sosi to lati bo idunadura ati idiyele iṣowo rẹ. Fi agbara kun Sol diẹ sii si apamọwọ rẹ tabi dinku sodo naa ti o \\ 'tun n firanṣẹ.", + "insufficient_lamports": "O ko ni sosi to lati bo idunadura ati idiyele iṣowo rẹ. O nilo o kere ju ${solValueNeeded}. Fi agbara kun Sol diẹ sii si apamọwọ rẹ tabi dinku soso ti o n firanṣẹ", "insufficientFundsForRentError": "O ko ni Sol kan lati bo owo isanwo naa ki o yalo fun iroyin naa. Fi agbara kun Sol diẹ sii si apamọwọ rẹ tabi dinku soso naa ti o \\ 'tun n firanṣẹ", "introducing_cake_pay": "Ẹ bá Cake Pay!", "invalid_input": "Iṣawọle ti ko tọ", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 5db53a423..25024d0ed 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -338,6 +338,8 @@ "incoming": "收到", "incorrect_seed": "输入的文字无效。", "inputs": "输入", + "insufficient_lamport_for_tx": "您没有足够的溶胶来支付交易及其交易费用。请在您的钱包中添加更多溶胶或减少您发送的溶胶量。", + "insufficient_lamports": "您没有足够的溶胶来支付交易及其交易费用。您至少需要${solValueNeeded} sol。请在您的钱包中添加更多溶胶或减少您发送的溶胶量", "insufficientFundsForRentError": "您没有足够的溶胶来支付该帐户的交易费和租金。请在钱包中添加更多溶胶或减少您发送的溶胶量", "introducing_cake_pay": "介绍 Cake Pay!", "invalid_input": "输入无效", From ab1f267c78abdb8d4e0f03d80b51ccc6b0485a02 Mon Sep 17 00:00:00 2001 From: tuxpizza Date: Fri, 16 Aug 2024 18:45:06 -0400 Subject: [PATCH 037/142] Update flags --- assets/images/flags/are.png | Bin 287 -> 190 bytes assets/images/flags/arg.png | Bin 340 -> 440 bytes assets/images/flags/aus.png | Bin 1390 -> 873 bytes assets/images/flags/bgd.png | Bin 446 -> 447 bytes assets/images/flags/bgr.png | Bin 735 -> 193 bytes assets/images/flags/bra.png | Bin 1269 -> 692 bytes assets/images/flags/cad.png | Bin 1005 -> 417 bytes 7 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/images/flags/are.png b/assets/images/flags/are.png index ae68c4ff2a1768cadb388223715b2f0dc75642df..8eb1e2bd3815aa15b34deb6a323f0b55a87df21f 100644 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWNC|Ka> z;uumf=k3*vyhjW;SOb=I1;reF``lAGa^juhwWp_idurRsDDa_^p`qSjpS$hK&9&23 zTyQXM+~j2JerHKQ=f#h0?qy%AJGppEgf+BHijTx{O_h+%uSl7>)1{T|63Z#BvoeKV k3xyUZKX+$i*tk%>YwzQm5~UY2fp#-^y85}Sb4q9e08`9Dc>n+a literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^T0ktt!3HD`?mYJmNHG=%xjQkeJ16rJ$YDu$^mSxl z*x1kgCy^D%=PdAuEM{QfI}E~%$MaXD00p;rx;TbJxF<`nDvKI8Wg7h8ciV9=TThJb zZAGbpulR*(!FP6Q8n!PEu5ez-+qT^KKlA2W_v`B=C0?{nWboYiPgq{uxv*O4uzH-{ z3Xa@^t9In=`0Y`o(Y!)~`S8})JUmK=0vK;GxhXUlo%C4z|G%T;7uLl)=iN$>u%0qg z^3(!-g|!t)>FFBZJ}_MV!mv7Qtu?#}nihQl@4AY1X$jVbis5Mt!)grOtO~c1gm<|g zeS!Xc4aQy)#^w|{+bYa;6;^f{PI&>sZbe_f7XpKabJ)ccI@&xQb92yh4BnN7z!%XM z2)-uKXWDN40NuKVfpPPz6DSz9z+aT|ldx9 literal 340 zcmeAS@N?(olHy`uVBq!ia0vp^T0ktt!3-q7rQ00=QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`Jpz0}Tuav6FIs(X@rhT9Prm*S0{_0hef<9JiH}dN-P_%JaelgpT+y^?3zb&dGGD+yErfW;=G8(Ctd>$TX{@F2}ntl1o;Jn&A}uX z&I;?j0;=IG@Q5sCVBk9p!i>lBSEK+13q4&NLoEESUOdUwpdiwkxLHO@X4Ap%^_La; z8WW4mb^jC;Ka77d)pgVVAh!z%w*(cQ>i?}R@@Stk!OOEzQFD3KoM*1d9$hOpo~ush zb<wn>X~kpFh7oHSu()$H($e-eoTJOAO;ayjFXwZ&ErLXgPzY LtDnm{r-UW|t%;#5 diff --git a/assets/images/flags/aus.png b/assets/images/flags/aus.png index c8837731c58a5746ff36872629dfabac35b11026..79a9744255c5642e75c8c4961602459a8c617513 100644 GIT binary patch delta 865 zcmV-n1D^cu3h4$RiBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!{e*+sy zL_t(|ob8wUOOsI?#`ou}3-v>-vISYtinN-yG($@Z3bWFb0u5$Kplzu%CrqZI+nDCC zBxEX5s3obT)ekD!buruKHZSwG?cJU@FHOR|&V}!mgZF`lb2vNa<@?z=-*e&dIC0{{ zi4!MITR=;O4iF8Gm=Kqu0bd{we=w0LMnId0sn3oB3Op?DfvutvmgEG+K5Uq`R<@hD zuwBoA$7qDhML}LWh+V>(e*#F$ZpEbD082_D<{nqUx3m;Ffb867Fh1>sIq3}M6mt07 zE`0s&K<3r94FLp4YoTxKhrL7s>%|PX$HzcXUNmS&BL~12$gx{k17-C~f7qo`SW=U* zGBijBYUr87L6M3PASeGheyMdZi{r7-q655Mj7?aPa-k(`!SmMx@H_6T79ZOCV9m;e z20?6dAf9csGWmGO9p-6>#%?Mp?I*G|nRHGV#+fBjq(+Jy*Yp~zS zgF)LL08mieg=yO&yiO-(D`c>wCSk0_0CBo5@=44W$dPfS4PU-F;PZIkR5!w$cp65H z9^{N5zpn)#ZGt08G&R46*Xx6OVw~Q&>_vCb+|Z9bhn0~7Xz!Sye}h_iI}A%&3LNES zP%3)q8SxKd0`U9XB|ScUwgD6cr=|&J@hO-zt(yu`6ePI=0O-B`z692cbd2g>;3UI+ zC>Q|o&KLzARS#f!*#l~73ig7$fIi0))wnI`4r#}c1xfAz0CJLARrM$?>BhEw%pcqc z!0%(?M)VE*VC+Nge^^8hWPKX~1WWF9lQ`UtjclE!36a+t(+;)e0S=w0Ls?}%>eM5&%d1||ZBYZj7s#P~@^ST!L+VjA0|muh zn4e!+1t4|O#zrtAHm=j53?c`V&Av#R)Ys=E)0ydP*>-v0OYWRAch0@@&6#uVAAlZ$S!ru) z6G=$mi#zB_d!~6HahmsZg2me$%ZawxK96i+q)S|oP7_@g?QlIVK zS^)1t0BWyZW%th(R>Me0e}ObHv&TJNfl}`8?~hqgA=iuOMe(ItkNbp6hU)Jx!xx}{ z8DTB%5d01VR~C2^_9GMj!aBf>f6sz{F-A95UW+kh89+hVBO><~|QQOtHiu>CaCovm-`>Su5Nr+>p|LvTMR zEy@E*y_varT|JUie}_-qudhtJfG0@~X0dHY1$gW0AhK&a)FUa%`mc)1A@tKlI5ct% ztrdtp^E8XmGefhcL|o3zAO%B^3?H5Jpp65X-{{uaEiW&}Gcz-pE@~UR-1H>)njV3( zm?TfFNbvi6Z-b|x3J#pR0oTXoRJ&>X^0!8pU{~=@_`0$he;&U4D}3PXV{MPrl;}t_ z)|Erw*%sKbttctc_O5T?ct`JDALDYl^d6;v#X%R|KYxaK@WPS9@X1sVeBD>nM$%FU z{41i;wLs3}9wnk{ zE^{dMnvZ_UBn?yVbew8n;P;6G s3DNk>?2y55cp*ZP0|~M-J9yyuAKVAP`)@@c&Hw-a07*qoM6N<$f`p)$rvLx| diff --git a/assets/images/flags/bgd.png b/assets/images/flags/bgd.png index 0f8c5cfe543515fb1c5b2e9520656dbc2783beb3..505636a801fe7459a7f7fd7000f6d74d014e7cde 100644 GIT binary patch literal 447 zcmV;w0YLtVP)@?DT-0OfyH zH7_Cc=b7l`iY^jK4}%FkhhzHgj|$@&_s&yUjA`W;n5B}IK}@uNvexgxm`+pK9)R6k z(PEfxEc~}`ql|X)762@26mNVTP5lDQQdzrr<7p=S0Q7QItY=$*k07?+*dm>kZAF|GDNn{1`D*}8%Tp6jjdJ;fA zoCO|{#S9F5he4R}c>anMpx|sz7sn6_|E-r^@--Oa-AF8!18XjzB xvG?B8RJCz?e)rl9>$?x{Yw>-e87#eWtE#f1n{TytM!T diff --git a/assets/images/flags/bgr.png b/assets/images/flags/bgr.png index a89509f1fe0a556e5c983509983685d07615e426..d89a0ae89bff348beeb221066ea35302e9dca0e6 100644 GIT binary patch delta 179 zcmcc5dXQ1EGr-TCmrII^fq{Y7)59eQNE?7K2OE$KXZqPbQBk74*we)^q+-t7t2=oQ zIB>XJ%l}d&5IOd^mFc=X0pdWpvU#+iJE3sa$ZW9uGwKMOXrk;>&)&cX`P~{76{5 zDy-#l*%g+7mxQBrM_3LdHe^Z^i6Sfmzj{O!yktrgnJz2?mpsjhJagd7kFUZea9yQV ztEKcOqLalpVH1W4NkF}Nf4yLBhT3DXU~Nn$GjGy5YGX1XcxHK03Kp)g5J`Db$)psc z6j%e)QT_^3ywqN=2R?uQC2Yb^4xY}PG@^iQL_dA_NVC~Y=~nOcNO1m6R=OgpS=$CA z_)|o|+QvGH?bD2?N~U75XdO|Lla6S-*=(X%idSI?ViK=>>WKK&fB$*7W9f12^6+Ji zLlzErdLnf6o>h=0t?aTPc+M3X|nD4qSU9Z=$)9Hi>(KyX|M3p-OF0ejN zxbZ?(5{(_-+2jg|X7(qxSFKm8RVC;1`R&fowz|O}K*@$?m3ahU#f8xeeV|fAGkJm&0SOM&vGb;b-JRYyqoqmAUKdzex zKVCA^V6p*@yKYyq(qOUxc&-;K4JHG?%JuBqO9(v+qP;tU%ltO3;?Gz z2!fWBlk--Gj|;jk4ZtZ4+OV{wI(s6#D+U1XZg}l= ztyBOaX?S&AD>i^#8nRs1N(J!$A3>R}Yo!Dzb}7V5Q R1ONa407*qoM6N<$0fOwqHw*v( delta 1264 zcmV8N2 zf~ZFQL;9+QQZ?z8hzWoUi{khDm1Huhc)eZhXA7i9{m0>Bq?9@rW^;PNy#Q zGntHHv)Od{9{tef^Z9g)gLbqT$CdSPI84jS%f+FgA$@js7Go5RL_yG!f6Zo_tX8Wf zm&+;nd|qj7Z3SuZLDZX?n)FyKMzvt>BkT3*X_}^Uz1Y^)R^YO*w6v606h)Yrn7w6V zW8=&pUn~FcV;7wctoeUF^B$M^Kp;@ixB}CwY(@5umeWEK=;GP4@GKVapvqSEDGQwd z`kl{(oCJ)VR5~Fzv^-6{fAg5;yH8(@mDY9-XJe3oG@1 zuudbNKA>rq0auL1R=R0z*LtM!?RI-9d}`Dn#ywAj1u^B5KV1`If5e@?JB37_+G+gD zyEJ$%Mw5{aA@w4aN)=mLT15TGK8l5M@9KR)6eMp2`)FqVb8^4oBu8s=nLd61)|NjP z@rCO4U5CSQ(4ufy4!X5FF)3+9BNEo@fNaLh#6zwZBinc5%oFxe~%1Xx2MNN zBj3D9(M6pCzm<=PABOKvKI)*EO&1-8x~ATt6{@}iN%NE^b}}^g>k8fcc0-V!n*6M4 z|L7qWEelTw1=WB*{$%O0m@YF0uRSBA3=W?1N|^g)l`VR!f2dW}8~T@qs@^0k8wj8|sS zdI6FkE5i&H3Br#gi!#BB39z5F(dfYMLIze|na#bngXxkPO7n(pTZPj~ws;nd6Eqnv+)EK{Z?Ox}~9P0^8*HTTjZO z0<1W;;;w5|SHed>6zsfXW|(XqQ(wx=8--kKu&@Lci)W#?VUj>1ke^D2!o)2Kf)z#GkFROJrEnqHu zhAXf&ak$eD9PYT(wzjr3-uG4Hr+ca29#F5Avl2x~p zcDr$20?}eAjxy0AH|E&fS@;DXT}27ufe{$fC?Ur7eciOIb6O_KU@(~3Wk_8q&_&tV z*(t_CMu}&s%OtZdmuoi~jpo3K_K(YzP`WTMUmD2g{R;`z{I1d^gX!`@r79N^(5rOu aAn^)nJ*dJK1w8iv0000st9#*5I==d zagWuzE$Q7o5{rKZgEd7jC(i>fgd{J?FYljt9z{_UMg0fC_b`Zwr|?$|1KxuGf)NOr zLM&qOO&vt-V?<|%`RqcE4;>_{f0Ib76Xpj#4-nU9jlOYx&V2yYTrm2ot&+O{YCAx5c>=j#G5X4bDxPoJkb5ic0U#*!_biPV zebe$-e;gFt0Fa+LjjhSndUgPBz25fnss1DG@3KuH{oC3CMCV7@rjRa3Yqhgz4ba=1 zM|gEgfT2>zCK6x0yvVwZ{^W2cKQf^YFOMRvj3eHdL9bE#c=1N@bxyD8JMiZku-Dq6s!YCL_G|{+orEXZ_!mSGvmY}ec8WTc_DJV594aGie zXU6m0^p3-{&`xLkB{%mmXKufqe|zS>h>oH(OLKE`Dss~_mE&xhB-L);;$792s}5UB zm;hopDKj%ON}*6trlzK7eSKX#LwSFHU)kE)G97=65{ZQ9V_;ywl=h`kN$Kh7G39f- zhqmeIX%qdR9c}i0WqD&`gObUlF*`eJE-fvg55q1L0<}t|5)6exTD4kLe`>Xw5)OwU zTKpi&!C=tL<#Kcq&Ua+FUAsUaU~<`rMx#3C`r6uBO;Hr#Vq*1LI-Tx%eX#SDtD`3g znf$U%evrH{+R9}6)?i|S^V;(AvL4_9T(7bh)m9?gMK2tX7VaaDjPmGdJc>Fhs%+($ z(#Qw$@8413;|KBsyC#)ClFeo*5{U?1mPhfGlLxxuXQ}vk#i}C&g52f>7HMhnJoLhYoPzOl4bQsA z&x>bzJuKXEcz9^Ya)3G{yZA+P7y;dmpeDWOLl(S;!{%o zmON7L8g2}acIh}qGdvBsrP-fpg`A!&p_mHXFwE0V^y>At?PFJNaD3h-x5&}9gvMeq za?6=}kJ|b;U~~HZe?xK$oY&ydPJx$B@+_Wp;zI_p~Ab%#)bh Date: Fri, 16 Aug 2024 21:09:23 -0400 Subject: [PATCH 038/142] Update more flags --- assets/images/flags/arg.png | Bin 440 -> 1183 bytes assets/images/flags/aus.png | Bin 873 -> 1834 bytes assets/images/flags/bgd.png | Bin 447 -> 913 bytes assets/images/flags/bgr.png | Bin 193 -> 372 bytes assets/images/flags/cad.png | Bin 417 -> 788 bytes assets/images/flags/che.png | Bin 855 -> 371 bytes assets/images/flags/chl.png | Bin 351 -> 753 bytes assets/images/flags/chn.png | Bin 860 -> 840 bytes assets/images/flags/col.png | Bin 217 -> 381 bytes assets/images/flags/czk.png | Bin 915 -> 830 bytes assets/images/flags/deu.png | Bin 703 -> 370 bytes assets/images/flags/dnk.png | Bin 801 -> 387 bytes assets/images/flags/egy.png | Bin 431 -> 553 bytes assets/images/flags/esp.png | Bin 1090 -> 1874 bytes assets/images/flags/fra.png | Bin 700 -> 351 bytes assets/images/flags/gbr.png | Bin 1341 -> 1944 bytes assets/images/flags/gha.png | Bin 432 -> 762 bytes assets/images/flags/gtm.png | Bin 477 -> 1691 bytes assets/images/flags/hkg.png | Bin 1023 -> 2007 bytes assets/images/flags/hrv.png | Bin 1088 -> 1421 bytes assets/images/flags/hun.png | Bin 728 -> 373 bytes assets/images/flags/idn.png | Bin 684 -> 360 bytes assets/images/flags/irn.png | Bin 615 -> 1150 bytes assets/images/flags/isl.png | Bin 867 -> 424 bytes assets/images/flags/isr.png | Bin 937 -> 994 bytes assets/images/flags/ita.png | Bin 705 -> 351 bytes assets/images/flags/jpn.png | Bin 896 -> 851 bytes assets/images/flags/kor.png | Bin 1119 -> 2594 bytes assets/images/flags/mar.png | Bin 429 -> 849 bytes assets/images/flags/mex.png | Bin 1013 -> 1620 bytes assets/images/flags/mmr.png | Bin 902 -> 1041 bytes assets/images/flags/mys.png | Bin 1082 -> 1436 bytes assets/images/flags/nga.png | Bin 193 -> 351 bytes assets/images/flags/nld.png | Bin 762 -> 372 bytes assets/images/flags/nor.png | Bin 898 -> 424 bytes assets/images/flags/nzl.png | Bin 1373 -> 2096 bytes assets/images/flags/pak.png | Bin 899 -> 1157 bytes assets/images/flags/phl.png | Bin 1179 -> 1565 bytes assets/images/flags/pol.png | Bin 707 -> 366 bytes assets/images/flags/prt.png | Bin 1169 -> 2611 bytes assets/images/flags/rou.png | Bin 705 -> 351 bytes assets/images/flags/rus.png | Bin 702 -> 371 bytes assets/images/flags/saf.png | Bin 1319 -> 750 bytes assets/images/flags/sau.png | Bin 476 -> 2201 bytes assets/images/flags/sgp.png | Bin 902 -> 1123 bytes assets/images/flags/usa.png | Bin 1220 -> 597 bytes 46 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/images/flags/arg.png b/assets/images/flags/arg.png index b941801284174a855e748daface2029e463d16e8..d8745845501576ded2c11ef07a2ecc62b9258469 100644 GIT binary patch literal 1183 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|QH- zr;B4q#hkY@qcdbmMUH*mHhbH$pbx$xs{(vmyZpI=9BVad12bEKvNB(LC?zFzOh_rblvar2=v4REBXNdd=Z7vuoy!algEb17 zk8mq~Wa$)dmQ)arGi-Tkn%D5yBEgd1ms#pThpfSLiG+h*zZRbLIj~Xp#wm+8F=E?N z*EZgZ$!SPcNtBfOvXN~?xLw1gpS(Rg4PP+3Pms%RnA@o+yi0wLf4J0yng4lj9u;s} ze&1!yV_TKq`x>gYiruuN(@W`{m}!KifLq-4C%jtgkoBN=Meh3qO%tU(|4Z{5QeOq=g(}qTmC1S?JwvZrs5{+z;oSRk z?5)ebtF8DNEwDF$>+LPQi0!K;#__(HKfC4oJ1*uqp~6O0|5$Y{g>e@DF!*xrZosU0 zhYxK${$PDAXMJe?;WZ&$cPcEeY%3Rhe}mt8w!ppZ)jS5145l(n%e(%ZHLh01v~AS_ zGn?)g{B}BXDnw6R&sVrn!LT#_b;7Uullgrw_QfVQZ2o1js{Qk!W4FF5Uj1(v^mo3a zyd~@3a|(aH<~zUt&+@Q1m80Awxl&VaPyBVs2W#pk1&Al4CVBXqDc!2$_;p4>$N6~7 zRc(WOQ|0L~EmI?B&sT^^Z1}ui+{9SuME$A_TOQQUUvakQ!#=KK4C!;lcnhxWU|jTn z%e0s{P^LPxJm3*qhq%)tP9c3ZV+XxGNgTi&_Jh0}mh79xfRu;9x!E=EQGDW;kF`(R S^~``J4uhwwpUXO@geCyffEt(p delta 429 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 JPDHLkV1fn|T~Gi3 delta 405 zcmV;G0c!q~2B8BXiBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!{e*sfT zL_t(|ob8oAO9L?&#t(`*iy-Las`LYhst9#*5I==d zagWuzE$Q7o5{rKZgEd7jC(i>fgd{J?FYljt9z{_UMg0fC_b`Zwr|?$|1KxuGf)NOr zLM&qOO&vt-V?<|%`RqcE4;>_{f0Ib76Xpj#4-nU9jlOYx&V2yYTrm2ot&+O{YCAx5c>=j#G5X4bDxPoJkb5ic0U#*!_biPV zebe$-e;gFt0Fa+LjjhSndUgPBz25fnss1DG@3KuH{oC3CMCV7@rjRa3Yqhgz4ba=1 zM|gEgfT2>zCK6x0yvVwZ{^W2cKQf^YFOMRvj3eHdL9bE#c=1N@bxyD8Jz;n5w-*fqH=;CUw`bE6LUZ4(a?e z7@`=$?F*MOdojEWw#aj+6MW2hO5d6NOYP#8Xoin4p<6MB)-g}}yN>V1)PH-KR{Yp> zx4|yo#!;6+cEcn)#m~7@HZZuc=Xl6VsK$Iu{mu32b6Vs9uN`_^4@_!r^IG_FD|j;` zGI0ny%wRagBB0h_z}Uj6;KPu3o%O?)c=6f;`C<-7#ow`giG0kN#rUFO>KD$L`HNdv z4XhHJe;v?x*rk6vJYc^0X{NA4stq4Sr|e($`O(k&>MQOWGG#Cx@qf63@uzE#y2gGU r*$dx$h4Rpa&Yl+ng&hM!L)B#d`8PhzpXs{vCy4Lq>gTe~DWM4f0E3BP delta 847 zcmV-V1F-z_0@ns1iBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP0008)NklMa0%8rT{WrjH9C? zqup*B`}_NJb#0Vx5I)-5=7ZbBLE0s#}ZS(tQuDC}r0JHx3?Dm5_R-NK-OGAU*Va+{Lv=bnfduRK--Q>`uUA>>yg#K`por?@ZW@@px`V_<4jyS2 z=@pt4v7UP1uk8t20VO8xPj0J4&W}o{&e{rlFxu|5TQn{7Z2Lqve>XRYTGtZ~@cZnP zzMO^Q4KcaseKeU&hCLcbz;AGM%kw5iFGbm=@ttouga{}fSi$&KX`Hf-% zaN!)zWp9G43?+9gf30e@YVo-*5DLMoAM|nI5lqeySNbv$9V2QRgtkS0(==tb+jVw! zc91WDsOKM{Ow{Ab0lT};9c)frl|- z-d1Bl;#=D?$$lo2>6Odn0XWexT^s+sNf4x>jP^SqM&G(y3v(&(~a4< zutfwZ;=-lY*b=o=v5LV~ixcbO!qi87prX-2lgyDj&>D@C2PQd9=J$gGA-T!o`|dg4 z+)F$U000000000000000AW`(}xG9?D+n}op11tv60o7mx1)V#B4xX5lzT=Z*fK@VJ zWHcwkqd5$)N(T6^zRHXFFBo7I4A^-nBVi~}?LM60G1Ru%fYivGtdJU>!vNc61G2N9 zRt3TU|a2_Z9}kQES{BJ@r5q-sT$?|K0naGI07KmvObI zXXbC%la6+>d*rN#+xK3}rhV!9&JtGt#?(G?|U%d{|wmHKOx1k zr=N2%k&ML;~l36lC!H(~8f}3}xlp_vL8KU}^>fY?BSR|7bC~ ze%^oVZtR-gBX{Jv|5>J{7csy#(SVJ6({kzR%jk4mYpq-3*1GR7f&%^(;de_0w5t3y z5R@2T^@XAXtiD)`AlrRK23QOrgsUOO6Uu9u235t!0I%hn4+D&Dhhu=jfJA4EM+SHp zpuG!)4$v1u7(x1CkO3xnMF<-VP~U|@2j~kSj39k6$N-bPB7_YFsP96d1N4OuMv%T3 zWPnLt5yA!o)OVrK0s2A+BS>EiGQcFS2w{T(>bp?r0DU2Z5u`5$8DNrEHA5Hx00000 j000000002M>n45yf861J?TPT900000NkvXXu0mjf=Z;M@ literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^T0ktt!3-q7rQ00=QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`!vlOmTp28viF&Pl_3p?2|Nr@1RB9d{>3#N_bol>TXK-udbyCrY*qI~FYZ14^Y+bA9j)Vf+BdBYZ`m4M*OmvG zou@x_6OfWE3GxdDnv4@LM7BN80xIMz@Q5sCVBk9p!i>lBSEK+1Gdx`!LoEE?p5M-O zz<|f~qDb_+!m6+T>(h-S_DFo@^kYJ@1h5rq^#}W#nhqT}!_`J|q5;^~>c&B5PQ9AF@r54V9B- S6Hx=&$>8bg=d#Wzp$Pz4<(P2* diff --git a/assets/images/flags/chn.png b/assets/images/flags/chn.png index 7a03dd26ea68bb9b9121578c4ed78fff0879012e..d326f7afa3ec0efc90e70d8bd2bd9a143d529503 100644 GIT binary patch literal 840 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|V19 zPZ!6KiaBp*?9LVm6ghT$#tOms6%z8@Tun(eETTRhI~)VKn_K&jEfLumv1Up}dH~B> zvuxF-wR3xWJ33kwqB>ibYAAIW)*n%CKRJoFa>m^=Rq5~k{(W%Z>ARbW&wjtVQ~XYa z6KFXcIE0)~Rhl#{vw`v4d#1w-?Tmr>g`RoY2d-|tv^4Y{YuELL<-3=*-`>kDGM_Oy zdI`VnrGC4Y(d)nb+fY)xxP0Lw!vm_9{;s|B#6imZkf{BBHk+HOw)eI7zM3YN^I0#b zkXIsduA4FI4b@8tcP%bttYz%?3flVQucGK$M)~SXO|zQWLrd7^#4G-l^mu&e)SLXe z9l^Wqyo^2YM=Igy=4&UnESeDJeW~K(_U+dsKYV+zh0*NXt7Y!yp80$p@%_salV_i9 zobl5zVMXk(yY{&ay}5d)58rxnWozynX2t*e8Mm7+|NATEW77RC4qG?u{Q2o^RPUXN z)b=mCta+u~GY_0hz4Umlad_3@#Q$5fZ)}y?`SH`QFFghprhS(hqGBSIYwd3B_z~ad zdHq_YxI?S?fkz?HyQcc+I8|hO&wo4doX+=uAGnSIRo++l`(}20&F`~CMIRn;U20g^ z*P4CEWq--K!@r%iBbeWn{;lTTb$jaBv;4(E^K`qmwth{jx0>Ykch1w4!~+)U4`(*b z$(>$zHP=2Y!+-nI#K*VxJdMcPe5k)jXkPRT>)_>=-|xM&*3ypm#+<)tmeY1mxoUsR zvtpZu{&(;9e4gUsKR#vdkL-J@ceH^EWY!I{24dzy2gi1#m7uufL4(X#t4y;1MTJfy9Ga z(Ju1?`vsD?Kk2iX9yls4lGx68FMMGI^B-=-Z3_RB0+?fJ90kirS9t2R7z;cJ0vQup Zj>J!$H*^2%Wk+g2;-0R4F6*2UngF69goFS9 delta 852 zcmV-a1FQVV2HXZAiBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP0008ep ztEJZ0*Xi)^(A2@M*XycUt?I;wQKeEbIf})imiTVBt8%%VmU^^-ue^*y@7m{tn zerTNMd7ABTd3ibDGB`Lm@Rd?VVq*60#l^+kPUGZ+U)LxdIDfbL;whK@-rind<_1ge zbUGc__mZ5e1XO(6qZwe0N6*DH#ETOiz(pwE_{<$CXIgd0Ae>yY^*qrX{5hNF0 z4r9LF50Bbk=ZvzCUkzy5f50L{>4^iASZt6hz3-A&HVcmOuLo4$nWyHLY%GcYYr4wT zYBeep3O9=^>XcWx*w^1P6m}Fn`Or4+zdtU}^|wqc$)M`ew811JPGcp({QUDdOSMlq znt$fss>ie_yy??4(QdcHh|jnnnTr4xP!do)=qQ>dmX?;VpWkr6e~(O98@{!&LyM4X zPZ)ankXgX6k-xi#VpiLe4R6ADqvR|Wl0Zf~k_KBr8X=<{5iONUlr})TTN{2F0r7a% zM~Y6gkY)`zxXj@Uk+CG$|1^8#O6*WVT<3FVgHYcMfU%e;#^})v?Rn+1Z)5xVY%@Wy|3kp_vLn5U7=vl~@wg@vYOey}Z06 zyI$I%UTndrT*;g$d6N%$4-J_QP9y<{%7(9*C@Kbg`|I;*Gu~G6>*VNm#Jcva*i5I|!Sm~FA8ZZ}JEsUB*D#;Nar~a!y@z$ls%4mj(7M6^0000 diff --git a/assets/images/flags/col.png b/assets/images/flags/col.png index 9a0fc6ac169953ac83b00dbcdef14529a6242e0f..798cd637404bfaeafc120824f91bf4a9c2531ed4 100644 GIT binary patch literal 381 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|TSJ zr;B4q#hkZy4t5@K5OBV@&vMlIr|oVIYC>P}?Nx+nQC$-$=eE$frk4ACj# zhuN;|F+Oy(p@Qo+_X+Nc_oE!YSKhK+pLw8RGOq+PAG2HdolT7V%$GFxfb7{0Hsc&n zM?2$3k$WJ=M1w6#zxVgAW7_(%*QUG@8#Xg0A4og!qAS`g!BAl?lI7ok8fODFZWEs% sz9=*xjz@aK^SkwR>bx*-Gpw2al)184!*<&}c|DMzr>mdKI;Vst0Jg%72><{9 literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^T0ktt!3-q7rQ00=QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`B?5dxT>qbCcy)kV)V7Afpj}Y6VY{&D2?0HzWXq&w4}cU$NswRge+Xc> z*St3pD8^af5n0T@z;_sg8IR|$NC66(dAc};Sops^wUd{@fQQ-e`PF~YyDu_a=kO4` zrjxic>cIMoRt(oUcjzfZnLnsvxX!&NXraqgsb|pwjmIy3SPC?O!PC{xWt~$(69A0L BN8tbf diff --git a/assets/images/flags/czk.png b/assets/images/flags/czk.png index a6c13a7738cb9601c3f9279ec6e887dd752ac6cf..e4578e86c30c4fea54b712a01db32c9a760e2574 100644 GIT binary patch delta 822 zcmV-61Ihf82fhX&iBL{Q4GJ0x0000DNk~Le0001B0001B2nGNE0OFW;IFTV2e**AH zL_t(|ob8xRNK|1IhHpU-ErN(zwF_!l%UaBQGY*s1_%r@cSWsqdD%(hl><5dWMMVTg zP~fU2qD5wpl0&ilaG-|sy4Y#z@egb+dq zA%qY@2qA_9Vpq|g&Vg=Fg`;Gc?ki*9B%5v z%i&MF1S=%KpY*QBxY9a=4>PkAVFd&PQ{UK&-q%xYkGo;E(py?MVyJp1QrT2+*3ky)X$di&FP69a;fq zQR?m)_$a<8wE~ResXGz^e~iPaJ5mCSpSr6%arW{nXHs|M1Q^FscVq<^M^bmB1sDfX zw-W-)qSXB^VyW9H0cIg}=h~xl4bBP3R^-r?P6075662JB<{eG=9$zWGRCNNXDsG}b zwN-qX>I5VzAK}vWqxcbDA-+g;0&2@!F;ui!e1Yl&B(sIT{%35tfB5p$3D{S54dcm; z;!9H}AeC*$_2T{b6I9^-ZO9eZ;CF1P_>$BKI8u5Mvx#-$ z%TXtwID4<~*T4|lSX@CqjBODGJ>2%r{85yDFTPfw4M_*^b$bar+s zd5>}E8yg!_mvEiDxd!_X2Fv$wXlw*zazD?ga? zAo}i2g}Gg(c7d0Fb7ErR0=vaXBvR73!P1-i`}?Ml1RlPK(8Z}I^ewtW?EtGd$g5`| z4e@Ns3(hIa8@%;5MdLSTf9d}0Yud{kP^;nQWY#j^>B2`Ezcowmzs9K5z()w{5JeJK z8vu2$-F?ny4h~Y0zwSebhiCf5$0(e|wtN_BN>@IDgJr zZHFkf0y;x=<7(n3-TC!_LZw4$BxJQ6qJ4dRl%SL2>|ur;Y`&vQnLpHMK)gU8P_Ae_ zxE}vbH+DWXTtxxIbJ{*4NjyulG@~f38-Pv|2?0ewo~g>ZQjq!yDKR72V*i$>zdY`&v;xfY}=x8zs(y zEeUFuiUM2F7N6ltoR%m|RN3KaJhQwmL zWmzhl%@&7-hHzd2QQN7Wn5fO21(rL{19E(H)fSB>Mx~sEVMTNIAH`v^J(Q&yFoC^c*rGY#jT}YVTca<&~JeLk2Tyf2Usxesnz~DiO=iwL_8SbIo!N{)?GUio;%oM=z0VpM zw4x0dr!ctf-;v5($K`gu1SR2xdTZ?it>%>e7X z@j0bsJHsR09PtU}i}r$aCY+xvc#o-L`-WJDdmZ1JKBe7gu=RJVsJCtSZDPB=QlVkC z?hJ-?5QoVzE{VROr;zVb?O17?b6~Ezat*6Me&TMRQ{S>aJ-dOid|uB7QHMRPZ-GwT qB0k0ZFxwrVQ@=k(_?O|e{YUnuyXJ*KVZWF`LE!1?=d#Wzp$P#09f)B7 delta 694 zcmV;n0!jVy0>1?ziBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP00071Nkl$V5Uq5#ID+B}TzbV{ za3#3#C;WzCHZ#BA|M&&7b1Nv!Mpq(omx2%Q(rwd^Or2<&*wfC;GWS4P)k!5DE9qDj zqGyOAnNFt;w3Jf3@+lC<^#8|Lf5*^H&Nm~b05U9MJRXbd^(qE~0j*Xm)d%~0K8xLM zCv!iH`u)C|quFdq?H>ezs8lLa_hTH|Mx&9$JZMLoIj^?Y>osjQn{+an$k}X$IZ~4- z2)bbymdfR_8%2?b<5<*cHIPz>ePQtsNcF*?cQ}gJA8%w2-gxy~Ud03tj&9{W<&tGEIXPrG z@Df62M6rpDjiG(D zz!$pP?W(z;@vMEBWZ3C+Zu|W{0w;Qu%SFg84Dy!-`uTAo;pF>@E*Wx{7Y@C+kbqy& c#e>FgB@n_Wbe`jN3;+NC07*qoM6N<$f{4mSssI20 diff --git a/assets/images/flags/dnk.png b/assets/images/flags/dnk.png index 69dd1b2b8d6117df195668916b901d61126123d6..e02d42bfefa7c501c7e03b7dd44f4828a1f129b4 100644 GIT binary patch delta 375 zcmZ3;*37Kf8Q|y6%O%Cdz`(%k>ERLtq!U1xgAGVNno({sQBk~}(bUt$F{EP7+gskN zm;*Un1J$4XQCE(UX0hLXk0ufFsy`~LoSzHoyG zgX9N3RgEbNE-{-tyx4MEB!!1JIpZd?PExX9_G|-9B@aPYZlzn$AO!*CQJn^ z4=$!^e`yeFcpDyJlyF~gDa%`HXMT(G#=bu|4*b&Ak=*dUFjtu|+ delta 792 zcmV+z1LypM1EB^XiBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP0008ENklwt}Vz(KI5y7fC?yPf$D$?`|`*xW{^E zas(6h{a~5d-P!Ty?B47jfJ3ljf2G-M8YDUA?3B%sVyO0i7;7lf2-yb21c(e4Rg$)Ta8C zdcWU?!C>Gw8V%lRwaACB5+#DB<2bQIB4O@!JLbABOQljoTKpjCu~>}Hf9G@f9l~Rz z9@WmWERMRL%jGEfb7l5WDh*EkI3l@_uJpS@AvJ*3t zI;h`!aR~*?t!KsaXMrk+$_1M!-G{%0l}?HBuW)P9lO-D{6*$-5BRim}S0m{DT!FV- z!MP!4J0P0R=i$?%Cy^b5_xd02KAd&0C(Q1yAs&w(MpQ>o@G!pze+gf^9Z7{e z&cmrEvi#t3htir(rxxz}1}z2k=tugg<)L2AXkE#}MA%2zR)w^s@uq3=&1U14%VoMR z5m7(bLXoJCjXMmtZi54Cyt)cW1s!kEWJ8jQ+P=5lNIjWM3fbv&>@7mt%0jjzi^Zbw zMUv{$7D+mVLSZ`|k9Wig_SfZO2p$Z=j|Q^7e~>W3ea9XdLXQ^)oOqBRyJHUz62AZ~ W*t!&Ijc_3V0000_DZBU zoLSHD@J2Cn*QTOXuY8z!G`8}ZNid6jR$ia}c-g9-F8^$9N(P)i_D+4_%3nFrAl?J* zkGF&D`Prsa6|!40c3)lh|JKL-q4Ap&AM>oZF0lXn%?*WYKW07pZYpiC;0S}-1NAGi zK^vOBED}#~s6WN|uudw#-+kYnTi>64ZM=S2_g>S#%MIJRW9uvTeEt4;?y`CF<^Fsv z>c4*B-2alG->l@oLSb85hcGtLItm7qyDr{`}>< zlKfv>>)!P(ef@d&tsU1dBqg*mrk>|DFkZgKnAhOI7Tz|-Hpa;HI~>>~Kw&fk7)Hmy tVH8p4$|22mf(W7L^Sp@Q2eLSvA2IJ(b8=DX<9DSX2~Sr)mvv4FO#mrU=(hj> literal 431 zcmeAS@N?(olHy`uVBq!ia0vp^T0ktt!3-q7rQ00=QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`>jHd2T+a!ro#jyI{>_N!JF%(%LEzul57)ncdHCY`!gmjM zzJId+{gVTCkCtBAe9jvD7udy2bFM1F~So;zc2xMbVkAm_vU%i8nZ@5oP>7veNWpXohsP;lzZZ>6>& zHLrHedv7Jt;@@o0b(o>~L*}Hk?Is5ma%SC^cMS})RQs_1n(yte{ya@zmP#GVtm2no jdAa7{{$StgB|&<=8CeUry$hKKbPt24tDnm{r-UW|um;*( diff --git a/assets/images/flags/esp.png b/assets/images/flags/esp.png index 0193a6a44bf2ffce70281ce6137a2301239dcb15..6b12aff49d000a6dfdc2dec326ef044842ad74aa 100644 GIT binary patch literal 1874 zcma)-X*|@69>)Kp8Ow|i;}{{?Wx0)|Oxb3}HnzD#oUw0(=?H_I#!NCoNLeabQdvTT ztk?KBvP^M}ZKRBhHCeKb1{YCBZ|{9`KcC<8d)__Ii|55Rm4LT74m}A40N}VS&eHkF z{eDSU@aTWey@5J15i-s_3IJeIzXSsE3lIPx1h%!rxKIk#@?!{(q7gk=vJ>7rSNuV` zxW1sO-{pt&okmzH5CTL3O#QRg)B;?CjQlPuLpHmjPXMH%L zJa4r=q`s`|$II}Bv!{kpmwuw!y(wk}L;c_1FgNz*zV9h)v|6A6j0E_<@1B+k3m`xy znnZE*2Wwyzkewv5vY7!~JSKnxR@|40q&v)n#X~Wx%t8b7h5fr?{%RtAW-!x6fia=k zBHff&#C1VU8{c82DpgQ_a8XYV5uj2EKTjUzp~bl(3sM3#9tphc=+&2AR~;Y+vkzv= z({XTVUCXPsIyJ^BjNmE@**bwA%L}u{-%WW1_ukUOl&6gPHY?5u0&b!)EsaQT2>NS`%V*^uMn}>RZ zFU}UHXJqI)^;?|O6BtiVDv5gnA4Nbu^pzAnk-sbW4n-Pa7JicG@_wGHl^k@{@Y)C* zF{Wf~;|}I$THgbRSHYqUUyxIB@!zC7QT3MK$wwlZX)?t+7+_6BihQj?!ZiZB9xfUc zi1L;mG*NB;jTg+}(JrYgE+-1R^yi@4JW?uMu`cEs=3Oz#jQd6NRX>RASFav9Flh1Nlsxf+RVL=;o397@)H+-D{Z;V!|xzmy6rB2Y6kIJOZmguw(ftvJIT zqHmm2ecGhUL-AgJq%qI60j8B#8uJ}gXMWy^)3X;1=`yX=eZbtOXjU~a3FaY7%#fe? z5&s@InOvnHUb2uyaOq__5eX?jq88baODMA#$F$BH{S$fYf6=6`!IG(sud|9HdfOP8 zx&dZUv9}t<`#+gGalDz7?470xTW5)#z!wLjfzGd+fCcUM2Eq1~;W+TI#WuZsKp zGd22)Zl`N@W(?WvY=>vCJ+Vu&U2?%ziWk@-F%UES(o;F2ru1qdRO`6`(~#8 z-mNY5B&7J@e;_YXH!&kT4q-(BY#1y|XiF8puMe+|aK3)+1atE)qWYhG{=TuE9RnP; zK{tHsMQWbVB46Ov4PM084^JU(VNf@uY6l}q2R|s95c?GD$indvk@G>%iXgG{00|RxG2*T-)OCFClUbNz16)c$CZJYO4ev)2Vb@FyJf_ z(e@upP=tbe1qY6r|L_qXF~_!s$rQgb6vR7f<)5_UjTf0*$zF}0@=6Q~Ro~C(?;6_S z`nFEhy)&Z5(5SQqvB5fcR~FQhh9B9;iHPS704aJidO?W7zzm%peXnh#2O}E s7uL1~`2RN=4i}Xi{+BxvAeGd1P^TMF-AA(>as)}h)(US~gY~=pH$N0(^#A|> delta 1084 zcmV-C1jGB%4#Ef_iBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP000BmNklNH8G; z2MY(^II-iO_})9rxn!&x%lQF0f71P=(K$17X8ifgnYnWY*bCOKOioTJBsu47Kbtm1 z@!B6UR`E!s#kNVz1(6YjjgODBQmMq!=``l%=G;EgH#Rod^71lo`C~|>QtlkRy}ex8 zSF2SP37GunHF$8-xo6Q=3 zK3ID!8qUB65UgMPb}@BcsMhrKw5IQMBXf7Eq4Z4 zwu&0#NS_lEIYo3D<#O2xg+d=j^vd6_U_Ldl%`+H^$MJHlh^_BSxHN{2BN~m;dG6}& z(#wE72<0t?KmJki_2mZiJcKv5 z7LlFjh?xwhzxf^Z>n!lu9SmHe|9Tb2g(DwN&+i#{_FoTPJk;?xat`^xN%ZyfARHaS ztC=pW&c?9*sPkTxtKB48C=?DH@KGv=E_MQ`uWCqdufsfh3fFJ_i_O*B;>37@okA2n z2^*b;THIS=VAm}myN*LbqrTc821fqjoiV|KpDBuf56T*LpM4H za-Lxrdp&v>VqfT}I`_~OdKba_A)F9oM}*e;s8&kT2nvPnX_RLN>lV zxzy~Vph@Byjsx-nFG=d7qocvU9_2RSBm8_icw%vJu_jc@jZtDvmlF64^kpJ$1esKU z(hT?vVk0?HcReZ+Fej7A*bcaF?s>#T2f^}rn^KbmB`bI;kPw80X zgu^?mdZL#v>|6ZUqUsDYPfC`;X0;@@NrsbTj1!%IN}XE2mLpqp<20jhO*&~;ldj$B z**r^Pb!W2qm7+Y|=j+~PyneZHp4~5oMmJWe)e|Oa)GA!)a7bp6V(R5G30t5@8RuI$ fGcUw{C+sU3-ed5UuVRBZA@!%p%|w zS0P(>!G-@2+$I0w|M(MT6$D*sHe$@hmEZ#u$6+3ws>T}H$(VRIm%ay@s_v<7kJUXr zRRwShk<|u+fh4Jv68CH}nl$bIe=}DaQl@MZVj4sSo9Oj=;&?oYPNxIY=~O=>eZSv} z<#MS~e+=5~wqB!Jttz(nJWmt~1;x*49*uRoT}A8AIE|U>vOb&5U_PJ6{eE8!heKK; zHi;5J$M^kQKA(4jAP`{~ic+aWr1cMyp3CLbZnuMfAw7rnN9|nKRj9|ce_Aa<8BHdW zPza%249MPDuh;K>Y`*@)?h`zK)`t@O`u+`Gg59UjFERi9<$eAm%5XFqMJ_7jdWl|? zkCJ>PJmKy+xQh{WB#+{ma= zMdBEjBWr6x0X0E;lv&$`!{MMzk80L71|{@mGCiudp3)4k40tu&e(Ny-ix-OpM$+mT zECDin#vCrfttYbTCb>iDZZ;bilPyU{!3_OKzjr*$#nmK;8J+odc7PTCY$KD0Ow%o{; zWV6}SYmuZr+agK7(P&&&t5rap;F>OHAt@N7iw4}k79?c)SyquDRlJaJryxOgS%nA2 ZAB{+&z3z1gMF0Q*2>?k&PDHLkV1m3;H;@1T diff --git a/assets/images/flags/gbr.png b/assets/images/flags/gbr.png index 151f06db5d24edc7968bac82d4d4745723bc5ea4..8b53f88518fa62d52323e49d268d3beb3aa10e53 100644 GIT binary patch literal 1944 zcmV;J2WR++P)T;MwRTY&*~g%*qQnFJjoz zS&Bu>bi^o>wtYP?{M4PUJ9zyXSXQpcijOEju(tIf?xj_ zg*ewJ01#tYaPIApd_jl;a6fYH-v=>^nMw$cHv-4MdjRG2okiCJ%IYt`(Ju#JVlte3 zDI;OO4hOhC zIRxz7*^=?t+uI=e{8>64)dC2`b?^g-JDpTQ*quSXYtA$Q4jjD$7JDFh<9HnW=gxw; zs$4P-b9E*EcYd#X)*@`_lDzqx5)nRMmL z7xTf1$2vLs?pUF*t-D|aXlU(*?jBS6`smmgICgH6tRDrBzo zXj2F@ykvc_=aBMB&kzAJCoO*dl@c(P<9`>2`^vy zvNtYEuW8DIXjjT!}LZOsd zOU6UmPzS-j9$6!+(E0-k%2{qPrDZ{ZU0mMdD<(5yKq^XqYs>_#TbyXT8D>ID;G`4@(2hU$fAFLCj z_V7lg;h0JQv5}QTc;GHr*RNHctb9JzMpkB;k0^0_nG>gnsrNEe$CoUYwBh*7`4O@J zyph$GGO`kgPfo(bXGdk1YqrhJ5PA4Ovyqj_(;+>v;h2OP_Z?9Hsv6Vg+ba3<#2J;E zG#Xi%JReidG8_}2^`Gh%fV5?wIC40>W!GzDWeVV+w%Bk?f^>*mvjA#S2u2uLnF0_% zC`P3;Gzp+3g}^Oejd<`v5x}UFhAIKnrVzM^i(6M4`M`xHfZmh_l>mGS;pEq{DFo}P zl@RLdRZJnwWC0j0DUH-Wb0P{rN+BcO#nvb47GLv=FQ=Exmr^Q#VP@h~II zqVfQQKx(%z$%>^NO=iGa~^WES5&N<)t1E3Z`FZJ~F zh?Gl`B;?Z3NupAJleVHF7d5&v7!!iQBs4ZQ3Xw=esH>}ksi`SeN99l`B=~$jN%Lb+ zU0uy=l$V!FTt6O<3l@t-;`P*y-g!J8iR{pOdZ)C@%QG`GFgrV&X=`hfe?~?|$VNs9 zN`Qt$B4IL{&4y$$DWp;O+dD`ps zrUXG?E(WAGEG;dSd^{JObGElsCHL-u6K8&hp>HOj5IOKlJq&)dA12GUtKPdq)6ntu zmj&6empuqaI$ng18Y@Hxe}>@ZW$)E@YaP#ERcd^EJk7=ix?YS%qaspQt(-as;pTnN z6*Jq&aYdTtl>1Sk5a7cI+|lWQGdEHoNt=5xA7E+c0ko5bPfw1iejFiHag<^vI@D;{ z_Cn`P?S}}MJdynVe5 zu48;osX!+tCbB?pM{e@0aS|9R(cw#ms!3a~6Z3Cf0SMS80lcKve$Jva=@c>w$0!+pS7EbsbH zo}z8u5Zc|{)Bpz$y#c1-uVLl%IaW_Ap6}2xs72r_I>3AbPoQjP?smIj@WcHK@Oob_ z_%@cze1Nwj1Wa8_iGJ+p=K;J_(U`OCBul+YEi3LMbhCj17iU+XF z;^JaD1JbpJQMuTFg#Z%^p5fByO@ftFhda!Tfk42B>%K@sfp7gts7&wlKBPRHg~MTq zx{S(Ne;Zm|rEpzpZ!j37SS*%uxm0I~w{YheijpMSO$QK6MO9TGA vaUmgU>+~)eG?y15X;kb)~ikdBGQHyM<-ezwCPB^t2AeC@qT_jozr)pp85IC_W3(0W}wY* z@IZ8?q06do-<(PVcJrjc#QLczKa|8C4DyI13$!h5}O&m#?B~Y z`ocZ&V$+xIgFFWRSeRLuU!57A?U#Jn*pn?dI`#>D}AlmBWz%LZ<5#2 zpd)t+%Wnl_vdBoU)aSOad*mmr)!MMWc;c08@1xrj31XthRoc$*1ZH>43gPUj1&5GamEiEPAV}JZHqso<)cvK6mz2 z?OQ5rV7BW@-7f2_PgO;K7t|Vyo?5pgY|TyssTI$owHUj(C+6G_(oH+C{kZi1r5_$= z>WKTZ9Syxw=Vtx>5=TX~zD}OQ|K0c2ZJl2j?Q^pV{-vtHogW#d>7-_zC4 JWt~$(6980(P;&qP literal 432 zcmeAS@N?(olHy`uVBq!ia0vp^T0ktt!3-q7rQ00=QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`O9OmDT+a!ro#j)z8>DzINb$Ol!r#kc|F4Oy=;ix!QSA2xvF~Taew`O{ zw`TisPHaXUubnZgsSYbU8{?0&Vq2#P$Vq~%`gK9<^C?jcMP?y>rvH~j|6dW?JyRgs zi~aT%q0nB#us%bF8vUSdLxxNThAan$Y*ltb__vSfin$JCxm@chPN#aSWcWelF{r5}E+EySQ}# diff --git a/assets/images/flags/gtm.png b/assets/images/flags/gtm.png index 2083ad806201dab8c664d23f312a03005a84ae58..8841a352a0b0659868ef8e059b3c4a016305d8fb 100644 GIT binary patch literal 1691 zcmV;M24wk(P)AE}Z^gwrAZFOC{ zU}=$25XN#)lyWej#Bd0wGzbAKrw|fhMM26U$YDhhgdl8-L5_hOeo07;)?R&f44rhS zj$mhX4}Cr}?@aQ(&o2zWZ=Uyk4!>;MCJ2Hc2!bF8f*=TjAP9mW2!bF8KPyztL0;xi z{DJ@Ic-jB22;dh1!~>p%Y(YWUf)qIeNW##dgrSP$0g{NRBoPZpCM>0h zpp+sMsRc-fK)@J937N$v?BcgX~?LgArp{}n6xB=(vno9C;VcY@-D^q)Ok~1 zEw^XJM1U9ZRK&bviP5PW=$f*acP!#i+)vmXE{UMvLIy6c_o<&@?ylkM`56{gm*lC4 z=M|u^@iVfjj*z6=i`sbfiFJ=}jjB!^Z>fJpr0y;5Ip#O4Pur)^n>8FSkEYa;BLX~+ z07DZ4q?Y~_gIPmv&8N7%{_xt0a}Kv>6_3w{%cF3?C$?GJ5Bt7X;i3Nm50!fEuTocaf048*Sb3C)NYr5$>;6X11 z3C}y6?r=2ZcXqL2%Vc#*&B{auOJiv`#xrow8gP4k?9u)U^4+4yDHd!VmsOTgT&@VY*?GI z&r+F_j(xfl$NWXSu3r4gFg|4zuQJX*?+ueydx+xp+;uVS6C~<(b2NWHALYD3yLiCn zhw#G6GA&nYNYlp>r-{Jw{Wf%K^fq;s=a`wA3fuM#4r8BR48NaqED@*Q!{pq$O^GQp z6#pM7c%Qc328n{U5(TY#Jmfc=B&#Bh`g%P{MSB?-?cuyZ&rtuhC+r7rjB;;bz1Vhj zyqom8^~CLteUf9kD70UkL~T4mdPNL2`?zdDTP?;7-0q?BLJ`TDy~IVlN>yP7F3;1e zp4W!1V_#e#ru>iGnVY7e>vNJzKcuF$nCj+ynmQ__7W7m40mY3O9C`Cq;$QnE@p(Jx zyl$naDT5{F>W>cRHH;P`I;)zhwgL|1{*IKQNX~qbM6oIBnSbMD|03vr>L*Is(eDxe z>tB$Z7D2i`mc||fk(oP4Esok?*EMp9Jo5>L>VJcw>}^u=-a&0l;LzFk$g4ZXl53-2 zBES{}XtP%G$%!3&q<(|slE08#62+Nw3B;e>wc+Mp9&RH^AIY&(uQO!YMONu<5(@uB zk?AuQR~_O3Kh*=2@5kLGJE`Rd_%Q2r%IY#080%+b@+Q{aCi+Hu&{@<}TC`Z&jYMW` z=ir%lXt-ivMe%I<*|G)sHy2=VtOw)Y3P>x9rt5lJcz)vRaYpXk;O6voE)AGTD&B|b zN)-mH_LPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0dh%1K~z{r?UqkV z0znvuf4gd{tC>O&YKJHyAp%LSdG`zS8R`?+7w8gQ>e3;^z)R3SBPk`+wlFO=(sFIv z7PoC3f=&rDfzUjcci8zcJp0Zt3^@N4fmDY~wrG*zuT1#d*jQa~>M(TPOss8HoH{If z>xpkak<^igi?W7`vWA(cWeqM2!+b~I>N{hWFcH01737X~U>F+e^1a?6max5o1i)Z7GF91>^__ zLgX|caugbsA_Nr+5dv4C=vQlxUXMM9Aq5#)w)2o%cImYvyqw|hSG-m=@B-Pv}l zfmvtYm%MblGqXE=-~ZnG-utHs2@D2lBy3H6VcrPtPc_ojLRT*Kc4@#0yDc>LU2iBc#_j9nKc{_j@OF8) zN#Cnl0xTKLwl&1a*R)p>U`Uru~Gs`mdi>|+uZSVzH4|}FdW?3sATo0GHd>1 z@TRqh9|DA`OZBqx=sqxQS_Z~#Ye777oW=<_SPQIVtN~eF>YY}56HHrHNu_7S5nAnd zu03ZkofD1oC7*HfAh`QJ9|r_z6D8?D7s*5eSbv^I=X*Op6d1Z8dB)wP_0dpT0N&Jl z>U)I{MI^W48|tL7+g5)BT&;?FWm@y6&^7+FBiy)7(L$^C_@4l&chhlnoPXsDmoI{8 zb0I~RscZ#B6q@d)jz-niylfCF&&fwZ#StpjswO~xWS_jlYh_Z0AcDL9i{O6$C32vD z)#J~)sr>_=iKJj%F>m;Z3M|Ue(1@}uola`V)9rB(YUBrcXqLj+>Q_N>f4F_-NIDkU z^`Z<7RzswMu;J?SoNx6HYy;xCQ(#{6W4Pl(XGN3yk}|^D6Y)HqK%}dd0O!Zrus_p@Dr6}A}u>j5{}xS(eS984^j<`amPkVL8RwPmeXrX!Awd&_;f^Y!L$_RlANUo3hKDAD0_N8qqy^9j{uF& zoMTcbH8fT*Q6V|9dj_t<*GLNX3HUo|eCo7CgO7xu$5wFEfqM%~KqMlxiKJXGB8VW;nf_nh!kgr^pHg>beFagmx*_-hQ*k_LhQ+Y8Mca>7A@i$@t zWmH~Fvn3qR(RC?FAY8vDsUb863Q<|!)O$Gs8gjnl({57rBWHq0E1y>S-p#jaLXYqs zB7^`m$7kJ{fQZh|?$PL2+22G7)i(m&EMAK}fTWAQT1CY-^jgcRc}#$8+UfK__{x_d z!?bBB6==zBm^LwN%4u0ShsG2*;f$5X7-PB!JA!w={LanI1k-OTL4WKIlA-3}A86(u ze2#m-%eOXr>i3F~AoqY*=yNSga-?1Urfh$PNz-LB6mxz|%TRAx3;Gu%Bm;9a&3M)=q~GzuD{tC;|LSvXc@yi^ z&-v4eKfD{W1i)=FR7t+nQ%%!WrSB*H2pIA@<$dS4G{x^FE(vgCbqQ?P#Dc{g0T5%x z1ZbE5#aAJ41Ssza;1s01Gq?t5;?KAUYnTAVS0Qi&DDMd16r{W}VzLG>7z_r3!C){L p37MR2^;DEwp08I!%EDo4{mwwpS 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 A{r~^~ diff --git a/assets/images/flags/hun.png b/assets/images/flags/hun.png index 9722561a8b65934e625e2106dafa76132c2f4a36..397910aa3824250defc881a91dca9bf3418de7be 100644 GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|TSF zr;B4q#hkZSFLtsT@;G0dVru_VJ$C6qhZ$UZLV2pzoM$yvIC%}I5(*r&v-%c&oXO5K z<7N|Q0@s4C-O^?W913?26){#SpW;3>JCQlfUh<)4Lk0I~)+fCh`WpFdyfgl$v@B-ojK&=*W0rz;M3x1#E zh-W+!T@mAOuj4W6)3X~Gx6kkSAnLFOP0@XM{)D&7U;I47%=jkfAj<~U3%`!?N`C-3 z?J3hIHyyA;ZU7y!9q5o4J%#)upPD|U-DtS$?^eOBa1Tw<_kMPmj~N(x{vVMq)xCC- SdCev>kf^7tpUXO@geCxO<&RPT delta 719 zcmV;=0xvp>mQYj_wf7v87Y1{v2uC%0#*+#@PK!%%Wx7%X3+lgkgNu$w7 zk3rvTHexoLsn|b8jYdPSQ7V;`u|FIRBAd-BGmd%aYqeSm>!2Tf_PRzNkH<8bOu|m5 zqk6p_)(CB)An2V=r&K1B@y_S72!cT5^Ldcge-L^ql~U{Vn*N3O9HT$if6n)P#d=sS zmoF?Y!{IOxLTDEgv-cK@Mf%h4^iM(Rft-N)^|tE0VkPMJ`xl=TxL&dsrJ$4}Bmwcj zyXWyPUhpMNH=ikw9D~hClH#Fbf5qL002ovPDHLkV1lns BP4@r* diff --git a/assets/images/flags/idn.png b/assets/images/flags/idn.png index 52c9659213385314b3c321c31eaace4332e5df9e..d1f01d8b52358d1da592bfcd2abb229923edde2b 100644 GIT binary patch literal 360 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|TR! zr;B4q#hkZS8#`G7IGhjOpYhjR`9#=*(2XgU&mE;a7FF^CwL*Z!(QDCF@t++Srs~dM z@M2gVe`hnJKf_D0b=)VUH^v3rsY@R2Gaua7go_^DI7?^z>*TC@RU4};(S z9qG(=Odi`e#5&yT_}26(?MA~+b!8i-4(f>5%fUR#!0_RpaD9V;Vsm1fl|M+-)78&q Iol`;+0F{}y4013oTcCoD6OSGx%1^ZB`x)yv&;u`CQTD2xPrpSNtC1&^Mh<73M)d`pkK`z1!`?e!oxW^SN3smzX0piGrXThGC^v ztGQ7W$vBQ>v)Ke`{SVQuR4VFvy^8;#IELH5wevhrQ9JE+y9w!Jv)RN_f69Mz5PNqr znZN}n5@mys8&BlCKO`O;ssE7c9r7ab<4afq3iTI7&!s3Pcy{;&<&-NdgUQJemIEIl zbVd|OEG)z2auK|GWJK|m3(L@IwK7gthyM9|rrfrgXuI9cdz2H_W@w_Eur?zfdMoxM zLqy5)gr&enaI`({==FNSe>T8*)oN9H)Dsp1&ZG4`Tkx&^kob(tCu?hg12v#M%B*d} z{eD-bM>T634<*={Opof&qs$PNftIHHUQY#BdN>>ss{X!(B}misn`*d(LJzVkSKN{I z&gZj7#g>CqFpqv{e@YLtI743XVWK!k(bfdpV!Z3R%J=K8n&+SH|q4j#b4qgPdWq~d9U@*{gLF1WiiDcOC_g|;e zDFP?)E|<%YT^JNE4fy$eA>kC?m0dCvE-xJM;X(p-Wfu>OziJI8Xh{@s+W-In07*qo IM6N<$f|&p~Z~y=R diff --git a/assets/images/flags/irn.png b/assets/images/flags/irn.png index 151a039194fca308ba1f7ec415d6cadeb04737ae..ec59e48c35666557441d538bdc037275e2338606 100644 GIT binary patch literal 1150 zcmV-^1cCdBP)j?R}jYi9|00^xJ$@y-~@vZs0B4|K->4;=u2Bv z?v=_dp#s`UtqLt@+dj2V6|{kd04jJLUx*z$cI>?+n@E12YG&mn<)L|sW{>@S(kQd% z?1}cw@0^))oJv6u1VIo4K@bE%5ClOG1VIo4LC`=cYvl-*TelbhfTg>@0Kfu(flhHL z)PNiSXs^O7c7T2$vPePt!9WeL2G3B0EdWqog<0$X{Xk@qg7kxd8ek2cp$J<5puP&T z*a7;1$RY*l2P2U+AP83owz6(90Dw?;f%OG|_Tl55Y9zZg=mv`p>$52$|WvKaf^#(n%w z_2kFoOg~ls?!Exfxb-#JTek`X=Y>t}2Y*U?=kLdq->^Zf^Jq%i7=)WIP-kjd#>{{7 zt@`&H4PgGe?XWerqf$u6KTvXWw6p0kF?)&D(e6xjZ%|&$Z7_Q}u%n)#qU>0I$`i@*8iG zztExfUz4)3`BdW+%hGRe-YmvGA6L0>=QnMVQxT8hvGMLmh#3VSU=;C(q3wf8v@L28dHy%K++Lo3jt;pqSWRwcqwu!ON zZMD;9;u?`uGaEn{8UM=3r<-04u!qu#{N=N+0Qinat)G9D<7xTGQAt1fp&>Ee1>DAc z{0_gX{k?q$An1Wu49S~siO=@~HukxVe*xA0zP<+#7;XR%CLBOFlO|Yt-C_U$8Qld2 z02u%n=oFVi4M+ok_A2aR2j~Z)ixi|E4Ag+k;2DZ=1_0DoVHZ0S0f*=TjAP9mW2!bF8f*=TjQvIZV0ogs8bdqQ9 Qr~m)}07*qoM6N<$f(zFmVE_OC literal 615 zcmV-t0+{`YP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0sKisK~z{r?NmW) z6G0UIW@dMjHkT+xu=G$BBGf}~y$Sva^{juO*Zzebd-3YUi+Jr15WMx4TS+lq#2^F- zfh8MfcEpiaNZ%-5p^ixMrmjYOI#_1NEzwp z&AE>cn(1lHg*Ekb=J&LZPcPoj{4E%L(|#8o-+3sUH{lH#yqP2OnsQ8sXD)-$28oH0 z+L0KF=zx(U9@V7ZI{R-u@su_5dXtFm%kV2+KY6~~DBY;=^W?wvB#5rT6F z<1sW1M6gyu@9fAr@*IfhP2@XGgKO{UO|vpY+{3@XN_`yv?*B_a>mzQjuW|S6Y`M|y z!9j=*4~26xiPvf`ZX1=+3KdEL@C&cMcXNVJ|C9g#002ovPDHLkV1m^= BADsXI diff --git a/assets/images/flags/isl.png b/assets/images/flags/isl.png index ed545e9056f6309113a5556ce16425a617dc4251..d24f29abbac82b2957a536cf05d85123c26cf4c2 100644 GIT binary patch delta 412 zcmaFNwt`u)Gr-TCmrII^fq{Y7)59eQNGE_W2OE%lG^5;NqM~>`W0a?hV@SoEx3?Vg zS`2tv6XSWF@>u5NwYFF^YL|vzF_PMP=t9l@C$ikf_{#p=a{+3Fg5Zn4PG~3H{iZ+B zd(N?sKUd%Tyxy5{7t;@>a~GQ0#HOS*DBj_DSioJxE4yxCCjW*G)9#2HEuMnSA{#F? zbrkkWY!7xmd9l7(=5F4Cx1X7-YMmsF_S&tK+4ygcg=w;ckuzgaS@6Ff&g;8sKF16C z^RnI$IUrupCbs4JwgusPBaW;uTEO7_Rxzl-m2p{Kaw1y`Q^@>7Us$iGD{WoyE9FiR zTf~vFsDQfSqE)6cNq55L+hn~kYZNP)YFJpavBq#u-}!K9ZJm=v|NLbQ;|~NhTxmMf zu#B};@r{@X&%+Ylt?VY(FKpoOJ z&`-v{mIo)ibP0l+XkK*HNh5 delta 859 zcmV-h1El<@1LFoEiBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP0008`Nkl z5E(2il}fR0x6AhS_MliSiayfY?KZ1ctK9R)V0(L8_*h$8BTHe;AP#KS+8g z6ymK`3w{Of7^(ZUvn-3F?j{n63Cc;KP;eMyBE*2~&3e5a&Tl;UjGVXN3QXQUgw|;W z+RrnhZl64X=3xqUj$gwP@%5nf@)}Afm&;8oR4DWYM$zy?@*iRgXV1i0oKVG(t+=L4 z2pDuu1_T`R5t39yX%R!ff6#0;LGB)kD1GG+FvR0=#VOEJ-|O{UsatwS^~GKIT_o-3 zAB~@fd$%*Rp9@_Or3H(dE+o!A_;S$2@}vAk)dwFxiPVvRzXy>|F@52;WJO+GxZfc_ zxW8e%T)VG7ncV}?+8@w zIP^6>X~$sv?LrZa+}IUlYmF`}6V1nuARG?QMwGB~{Kl69I+S=(YxI1N_lxToJY1t{ z!w+0W(jm{d@?;%Y(1DsDA}U!24V_Mh>k$>KgLo)eU0r2*M8(w8H3I?$+?rIb7ZJq^ zm|Lk-@FeY?LtwzqfAJ1C!Ko**eCBe8(rPps7Uq3}mV$otBYn~G&@X4Su4u|g_xkYK z>X5cH-ZV|#@AsW#GD-I(BI>FXibP#(9Amgglwa}cDkK$jJVcW|Nh*5#)V446Xf!Hh zKA#^95zxhZ^+q`I_4k|W!;hqYR5Oq^hPU7iQ^F<{_*G?4Y_gM{JjoA=1z ldAu-S!Gi?Z&3kx|_z6dD+ZQImsn-Ai002ovPDHLkV1j32mh}Jt diff --git a/assets/images/flags/isr.png b/assets/images/flags/isr.png index 9f815dcbd42f579ef1f007da9f92055d77a05e86..f28dff1ad7067a4e7a44626caa9d7b8ed858b615 100644 GIT binary patch delta 987 zcmV<110?*Z2jT}IiBL{Q4GJ0x0000DNk~Le0001B0001B2nGNE0OFW;IFTV2e*=_B zL_t(|ob8%jPZL2DhWpQ|kqh<0So{j22}Gla-jG1NQ?Qp>K~0Pw5V_IBn$!#IhqO?k zAVjJN+KL5YixgTq>C#^?W~Ovos8}$iXS(}7$w{`;-Oc2@bI#d0yA}%o0000000000 z00000089tdavYXBFdskwjdp=Se*g^vm^#H-Py=!hfO-`&l_S7vb90#czGnA|W)EgCMaDxCE1gP3-3WO5UsGVxc2uQO{ ze(}Z!-rOGKDYi*IAJYl&;&oInY=h1y`Fue&PpVP<1u^7A38ItEv&(p91xIBx}(I;B`d}S@GkN;lJa$9Ra{^#;8k=YmpOUt}|f6&es_2Sz?3IhBs3Lf_7b9rmW7~jZlmWgwy(aopAtGhby z(`-^N(4|fmb>VX7xV-nHfqA5$-5i)qYy9-3d@g@86f35uo_u=hafB;6uiFup=Zch8 z7;_HA|Ly^Pb4;3mG04fFI?8|5t}+}#P%S-gBr9VM7skKK-i3H*dY#5khZK2hPN{PzXC zl&`r6_P%iOvy@vOmFxEyIszCz%u>S!ou!{9{ve-^sUm>k<6Jc?fX~zEMPPMihDkxp zr~=fg9bi_oKma!cpk9SmL;$@btVluh&Oi;Yna@y!Z4iKb6Vd-?5)oL}iu&@A!hlk=F>6J=_6$%CJ_%N8AofU0FqfsvXo6RN*27_F_r+Q>t zUS8(Z4%x|OZ&&KMTn_U2yt%r%$}^b^wPD(c5<#`yZU-hOC)G};!wkb*C%i^aMqy9Wmc24k$(2C`REsZy@F9x^qJr#SgOAnf~Vos?SZtsGhu!Nj21%1Fu+IJY`LvWAFN>FTTOn8y&`hdcAH+-TP-FaJCS# zS~np3r8m#sT&Y1Mq+toTj&39pVRA5u7ARqH(gnBJ-YLSX_s4?h`t@_dcJ<*Wn7e!u z?&0(Ljq~6Yf~ZVbw|KOcfE|ov;%pF}Jh&)qBD$Z;V2Sy+Te; zR`SqP;1)069gBPv@f~ME2klSB__K2@$;|3 z_V(GlAnyefyorg4!H9YRjcPfv)ALcF-Q?UQ_k&Y{f`_Btis)EZq>Z(t=SbQi_qbyB zB#kWf0sMp|=xy4Ge=Lo9YiI5^RI62so0y)6wI4Snv@#Jlg4OkO%=n&J0YBmX-Jprb z5uuB$#{-zTx3||#oS8bp^GO_h06dBu;Tf)rT~8YmK5)20sTGSw4cC2zQh~O9vLflD z(!+i@qr9RnBi-4C)7K8^OZ8P%<&8$eNG6j;Hk&1)rnQ73cTp3K9b5_8SfPnWS0Slj z!!2}akfdU__k9~u4~0TP?(gq6TL|eZ3;B|a$K#?clJqQnkz{*jW~Qa9eq&bb8T&}U3QmOxUj+@S=2~qDL;#pU`CIF^Mb&}V_YT_a{lEl eV1)SZr@H=}hVw5Jj(j%<33$5txvXK3#iBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP00073NklaW4f137xm@5q_Gqx!)4I+b03gKoF0*QnR)itT;h7o}23@i@(+zJ9;2XdUXOK672x7mEcfm&;^09IEkn zOlu@2Q6lIBK~N|bi%uAZB8nnWsZ@xx{z1|Ug@QU94)8l<=dk{)o$I;^fAyr%Xv8Sv z*=!aGA+(DD**n|q_RZIiT~_ zYRbv#t-o9@3F}r9t<`Gxe;(z4wHcZy2dvHb|Bfd*3T(tL$5XS}1lvH)V~@IEG01s% zzNa3w`bFY9F5j%J1zo5K+M~?cHk?i;WqMSzw((LzUnbL|dh0380Ly?!)BU#|6R>2p zTE(E$D_DXA)GOw2F>XDPRd>l9N_V&0xtMH83I+4(NBX1iFq1Qie=ELBWY@^rnqXU+ z?>LU~JTK~WI+QOF(Ih?ak!XUQA-X%j2|lK-T2jM>uV~_tq-OTtY(3V?<+7I3>D0d> z*p?gFl5DrzdM%Rl%(h4}Xti3`&1Mr4C%C1{c}NQe*`fhI-wF~kJC;{u$P_OmJSa$z eU0&gV@e^hAq{-9k4u5L^00002iBL{Q4GJ0x0000DNk~Le0001B0001B2nGNE0OFW;IFTV2e**$`cG4D`~5U0%>C(C-Z;OzlpP%|DQR>o*Bac0000000000000000000$D@b%D z)D6u81R!`P7y=LoK$P z4QE2@>v#F<)uD8s>`8ldRhDLFWpQdMP-$*1_)R^#JbW!%-_F>Lo`@dc`)%nRWu&vU zEiJ1DE#sKY*vu?(Pgl9%Ph#i5NKR5_85wS0knJYy4pUvJ9lf3xQgHhxgm zR7z5#i2(j){*`kf?X|U$o8OnSu_676ixqH}`Q=0+H1ltlB+d|GS%Um}Gt`;?rEJCzt+qQ07vDV65ugL7T=O#LUyy&Y`=pa~HI5D0*M77|tfzY&BI#BThv`WOHJ002ovPDHLkV1m8dlk>0Wsh&@Z`-8@Rp0cdD31y1RC_xfKXznP$AGZkdP0u z+3|hG&azq4G`m4qADF)H&O9@HfAY@v5Dh{MOZj}>fR$2;%Q!=l;pJaZ*YH@w$907% zKny3by}d0OjfPlXU#C*3q{`r*pP!3Lr6PSlj8<1yRU5Ojvr_Zh?Y4-=<5HKS9`d%f zwj|m?KJvWnYX9)?kjmwG+qOt1f0Gcc{vrHW zEGEy+&S(_;b+kV&&$2AZzB4yB*Js;5I5@C{5P#ZW_2%a0W}HJ_l@WpCI8_ebG$#vz zRDU@121gO&W0@iXj^Z-+p2@w~$G4#icubiTF?cl@6meh@!df~5gP20Xt~coC%lB0K z@QIq=zN@^%T9(pJcWCj!e|-vz)6-L$nVC_#+=yb9QwVgbCse$dqe}jjD-wWT-hD=2 zR_-ZI7_dlJ(?nel0j0Z-Mhej!L_H{)&8DOMiG-fK+Z!ud;9zJ3MJkmVjHn*4LGkPD z-q_re0Ni0vLiKhR1n9~jV-MUg-LLWAEKjabECoSerIIwgyfijHf1X)Vb%H|owkvdg zex8D2@!|MuWSI37!TT$sVUT{Zqat+`z**c4E)>D@Ml|$}gs!~x`SnP#CNe@wT(7D9^zaG>@gwz08sO-2maY!5fswbF z4VVDDx&w>}z%v2fYZo`e-fO_f$Sc(!npa4zR_ojXbaZyXXKr3lb?S`j#^hzPx#d25 zK5qfEJ$me%0vHSih*%Q`AAKW1Z~Vb{|h^6$ii4i+wsfS~Y5tM=P& z0vtV71^@W(95|3)MB4oW1K|1PB53%dQFSBpW_ax6Dfn=fE8M)(OxkzuwUG9qq7vs4 zp!ZoHaXkS`R|unL{idyCO1~T!w3&b96_WPl&;EXx={X;K7cGI;6BFdxu8eH>c+OmS z+}`0l0z}0p!iS%`!M*zrNc-ZI1~MDY)Yb_P>=P6QFeH{T+$wY{Tk*kS^`T}#lWP&;P?VeeSlY^qu}lr2r=uEgnM?RWx}Vf^Psc)sp(!O=)F8p(rD-@Ru+`x_yVV8=G#z2QyvBG#5t}p^;Hmna;cw zBewA9b;K3X^`NBUIGM@01;xUBr)p{;=Wu~z3Q%*tj&yiOdKPJG$H$4EU$OQp;5d$4 z%gQ}WruO}|hpHQ$pTdr=Zun%52kg$wF$tq_0j6Q};o=f9z-W}@Fait>55pWEf0!2# z0%K#w5wvq}7W~`A6FR%PNgFpb|C;R%*@yC#bzZ9l?$ktAYZQVv(4UO@s zpU*ETBW>(_$O>b}(GAJ^`vWh5&rSm_z!Ny1+0q!`34Bf(@Ph-gz6%YUo}DX%%5>D@ z%%%(Q!^cm^<}&lZA;%ElQo}XU!L2{0lD1A~^z*^tD}@Ct|46CH$dW|e1aKvJzz51O z{1oEv@<$7R*Gm^22Mf;L+WL!vC^&e;Q5();ag?N*02pG<^$!A9-vHszZQsE>6F-kG z2&eI`%xvi$9UcNcDNeEB-(X^ltl5x|ycuSD`N6>8kcsS;yl4o<{3oJS)sZV4_Jmr`I zXSyYJv5NqBfVE-MR;a&pMS7?is65&cvwc-8WNA)9#F}5gJLD`Oe%`m~1hlVQYrN0j z9r(UpNqxicbK{n678~B~Ge8!z(=0apgr5iO)l`F9z!`8^`14zdKg-aZ1f8Eda9I*v zqm}7+0%*1JHd$tjzhT39vxc9_#P#Pv7d&6_j`^JufWzYj3r9bWe5uB#%+yo^ce$Pk zz~R}Bg+Bl9+>SZA2cChB!b&CphX-pG2!JD5uJI{zc3)-!aG|~y34jrfQUR97USI-n zer^^C;2nBau>knH@BJb?5$LVh_@jZoij9x-2(DoQ3`8kk1D>pkvq*r25$Blz%{vb% z7NAU1Y>@ylNydY=_j(^tS$qQ~eO+W5U%$W&mPIRHfRSnYmA~74X9TFf(gVkn3uOyX zkz8bv@sYnaNIMUDrv=bz_29dt78>Kfmu&pT_#a?yP>n^#_YOM;+Hw0Q3p<-M;K;Ys_{j7FGTu)+n*jJO6%(Cq+`298{-M`GJdYcza!nD5aI^vQ z3Gs8erWnGb>nt`r4L|!{NIQtb#H^yy3g-|2U(w;YIdWN4Eck{jCGQ5L{BA*6n^lY} zh54mzlKNzEE|Wb4z)>F=n*c{Dj0arpon0i;j3(($N4?$v@hQLA-1u>tYoOCl|8hl~ z%S7LC;|uvMKkk&CU|>%HFjWJy1U_~1g6KZXgT2l<$69E)zZrJ`+~Un5%hEMF3=V%{kJ!wCn?aRg?rT zHYrC^Qs$%68{lwBD|lECa$>ZTceGWK&!@yJj^#0N@cZ>9lR+jpsD~!bUEUZ zr0X|tk%X+IE!%|TEffV4x8!CQ^!AOxp8d^a<0RGacrV8JQbSEj%Xoriw98=FR373|t;u?mdeOu}tvo#M6ApjD?Pjp3Sbb1Y=A#oudX31fc z^t8wRXE3~ks0aIxrAXLD_kvY2bYFwlu=qq=^Wabd&@vyfucx(Fv}9>?Y>Z@6uUWrQ zb%XOF(n1VOt)x*+Y~nX0c)-YxJ|9MWwC2I#1Q3_`U`eOB9WkvZIDNKOb;DD`;-V|A z=*Mze>>~|BM_!GRkQ1XKhfht!%6RNNR?3Ta#8`Pk@A-Q3cO**cACOnUVhiG$WUS~f z6w0k!d=`?bej0{i@rlx?$L<11*%4!S>fs+uW`o)SV5BAv9jcTSK}v;Grso zL@1PkHiaD8Tic62Kx{>N6Pw~m5Zcl|z@ELdCx3yqMO3gJdKC|*D4r}JW(@L%p|yoAi(T|yviqCTdClP--)+Z; zc*tgdf4_;^ZbwmPGMNUYvy0NBj_BX@HpW7+bx~DposuJ%$DTK8_lf2Ht zPXJ$J;F|2{I^i|9wzf>eFp?Ai`Fx)0g^XYzJNSa^4i68rjP*crZIBFgB**|WGc)XH zcXu~svsns*#l=O+<#HsmN8{t;zV=vx3kwUp?ASSU4gxqhH#f&oK_56B6r-b~+y{Lz zMo`EN2S*Kd6cG(N8KStff3(E1+zd4~+aC*5DVJ;X>2#WZbY*3QrlzK7XlRJSAR5#V zgJUv85zb;#J@tjFH|fir1;@$8g9m8esS|YI!g-eUR8LGyP+wmkkL9-nmY0{o3IdMu z8qg`o3vdc4L-dhCJ$-BEFH-sWiyiieY~k81Dm{Keo%es)B8me8e*-i!GQx7uOagF} zdCZR=u>lZO&9&RK;c`;P*6THnHX@5YV`F1nhfKYpq8RPy=!mVZu70JEcyuyL z`#!AI=*vbU*Ou5O(`u&l(}xrmy}iAAW3kvbnZXJ+lRmIOydhaOP;I)5d13 zhwvR9^-Dd78cqW9^YgrGnF-Ez)8e!D^zv=dvtM6VD@~s3f8n+|+0ak%uwswhU|AMt z8W9!XGe3I5T2T{$Vsx@!xpImwolR4+HSXvV@dnoAE+5}91_&M}Tgao7eKT;Wm21@U zyL!pfM9j|43Q5>vaBxsePfyp_6gR$$^-tv*z3L}}BG%W}x$TFZo}L!6uX*}r^8GDD z04c}K2`R&Je{draZ{*^Jy51#|NggA}Maa&wb9)NB+u`y^OPEc8l%o&&sxkb4 z+kTC%tE($60zy6&M7==TgQBgiEoQ&j97Le3R4VVpe**YoKSh{A6(ylIWon~%lE}3D zkzaRvwABMei~4x7U6p5iMY0SYct5=QspqO^aX)$8FEagl20OKCyX`f-KA(H;zq*IG fVfe`ZVob(QBp$46n)2^-00000NkvXXu0mjfRsR@` diff --git a/assets/images/flags/mar.png b/assets/images/flags/mar.png index 65b31c8922927a7de8b332a4ddf9af8c22f061e8..dda3a0131f837726ae2e041ef3c8e185a4f00af3 100644 GIT binary patch literal 849 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|PU& zr;B4q#hkY@&U%L=iX1y%v*PTHp2d%AUYyxmzCmiU(di>SN~=%GczZ2Bb8LMSOW^5_ zjwUl!R^2ETN2l&6x9%o0$Jr}ITR5=^@Jz64% zyt?Z@23YI2H$VDYeUD*l*!oMpck0c~N2lkMJ(JnDd-Kw7H@jn8y7$%IdX{wcnR@Km zM}L2vV{kp<-RmePe==+1>cg6GSsbrE?A^FM^nZSV(sB8u#E*8h*$qmMKD%GOW1U)f zz2`9Vv^*e@=}BuqgyW|q1?CfboH&Ge o1nh}VG^?I-BBw(TyW!6f|Fgvlk5Bxm?FQm|y85}Sb4q9e0L8(3kN^Mx literal 429 zcmeAS@N?(olHy`uVBq!ia0vp^T0ktt!3-q7rQ00=QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`a{_!qTo0=29#+?#X{>idO*hQL;E=lRQ8nESI(kVi21^X|;$00goD7QX z^}W3f{k;tLYv@ie(+~7CXtL4|@-$edt9MvUcekc)vWtOrfMK?yL7S!i7HwV2K*PzV zdQ(jGavcqJXzA|M(%q+_3$*a`(uX!cN~I*oFZe%Uz!0Is%L`P`S>O>_%)r2R7=#&* z=dVZs3eNI$aSXBWzkB{PU$cUML!gM`(d|u-ZlC)9KUvyJ(bwAd#*U2lY%}HvC>-1U zTI1sj{tSUO#+=xAjb0_LXmKaW=Qe(merz<5xM`>9?K(|7HS)`?Pu_0;QT zrL4-*PMxpVb(hLju{NhmsFuvxGVj>-{)HuRM|O3kwkfv+ZhhMxw7vFo#evLTtG5SR jgZ9WTnHTHWyOa5&v_*(i>NO6aD;Ydp{an^LB{Ts53(lu; diff --git a/assets/images/flags/mex.png b/assets/images/flags/mex.png index 9531a3ea2d50531dc9fb94d7bfd80ac3b540c52d..f81102912d513273712d7307e6955a42e6bffa94 100644 GIT binary patch delta 1618 zcmV-Y2Cez^2h)*QX}S~PME9+hO7vdydW~;jAX{S z$PFbsnjt%yrFj4iWQ;HJK*ly=hEDJ)v>{8O30Veh#7F2;JTNKyZTUA*)b>#g*cYE3-`=jkRSG(QULcZ&>UZa zK7Bde3wl}s9IO>}%~w#YyNtlJFJQJ=V7J>Qlki`|{oaG`yQ`z7fAj++z_T1dhVEM^ ztC!$t+-fv6$HRw_oL7&Y>MX(o2e|Q&Fyj5t3^&tVCM1Vo-D2g;P z?s?qSh1T8{@FjbZs}vzUbq7R(_tDv24cY$re?M~e&4tZ60!zOMte9WJ6jZ;L6T^s|~M2oxsb{d{lO{V^E{Tm^6p`y#~oC^ca-rtjvHeWGU3V z_aNKvF}WLV3btXy+=K2K2^_2i7iGs`GuM#-(@DZH1##%;GjY$6Hx%%=a0auYA`lQ9 zfRxlkB`I2b zqa9|O1LEpo_?P5jb^3X1xsrxAg?vs7_c65~<|JN^j|3BS0!L(p+-HZ)jqnOujH>2| z%8GKpH2Z*#<}64m(@~*Ua<9vbEIe{Y@IlcCjDA=D_uxIHk4 zn2Ic9f9on>AHR>D;U1J$Cqie)dgR}sZ^$?w-GnLhH?SgLC9bLpP~1F*j5Y_pRoby7 zHV40{mZLA)13Zy8n&j&ck$e`Lq*k;T)o?iMxZWhg`j9n=155 z!(BBA@T_iVdrKjbN@9_CEe(R)DCj!$sOe>qs=E!TvK|W47&o->JGTXaDF-3Fc^<=q z`f06AN04)RcC@^{3VY)H@iA{Ryd%DZATJtXSrT--orubh#isD}@I15#VOI`wmCtn7 ze)`w%MWYhti_yp{PQ>udi3-Q+?w+4;Jrcm_dagkNI7xv2x4b^r-r ze35U)v9tq703+=H7ga%Y1TmBYSn2~P2^*;ja#1HtWhg^^083sFHDX5Ugk9u@k`=*_ z7sS#$fXp}}xuGtah>;!5On)9g5ClOGCRks0cRm<<1jYu Q0000007*qoM6N<$f*L*CWdHyG delta 1006 zcmVR~n6m z;`jTRwE2UqyId|&EEe%Qw2zVcM(uQ67o;0~eSJ-m&54PLhN38@i$UzQTrTH+bu2JL z)xFpNqM_~hc=sv(2GhsS;E;4oM@f2nXd_z<#e5#>z`hoMj?K;9lLqI}BXFa(3a7AGBDM5UZZ z;`w8IO-`aGdKedP-ht=;0PJFUdD-y!e5T$tqDxEPkbU_OrSyBuOif`n@do8jDcrdK z40h4e)5G_@2LOJCqx zsY6@xAU~gg5)4Dp3K+U|&m>u^Rj{~}M=%h^X0Yk&Nz-&JE~fE3U4-F^;pBx81P`6S zizg4TbI)$LeL6)MRoKOv>xr)dJMeDtBQux6Q2Z<&m4kRXFOc0DhW|1V4n&aoIs<>u z{s(W(^%M$)U%0sL3WB7SxRKA$}YFxYw0qkNdU^F`8qICJ{iPyOfsx&;~I%E;QW z;DDOJL|c)$)3C6xAgmrWSvxT(;V+Zbqvq0cngNG_4w4~tf77FsfElS&stF-x;Sd-Q zvy{U%Y3Z4)_$9d`sps=~osun;w}LhFV|~5tVNK3>U-6Q$-abdWtrgOi$7`A*23ZNN+y%3GbgO3%bn1AFlc`?koDCE3AMei(<4LMJ34gdfE07*qoM6N<$f-Z*O_5c6? diff --git a/assets/images/flags/mmr.png b/assets/images/flags/mmr.png index 7fc6e1661e30095a674fc29adc152e21974b3022..c2f9fec10f6886b8d184cb7431a32d7ce5d7188c 100644 GIT binary patch literal 1041 zcmV+s1n&EZP)^Y9Ywm#VVxZeD--l%d1Wo~glk$C7Q@{)$ z><7XFiND4Bv6=vH5q{w|Ak1PdanJAWk^Nctupcc(v{%_G z7Z4cgO(`HS^jf?>Qw!L8b0yMI+ptQ!Ptyu$>^hxRz^%up#rrg^0N>z?v;z7+KNs&) zw*p+AI)t?l^vYOd6xSb~z-H$ha|#%*eGO~i|I!1!@qYL%4m7Sa_YRn-@(V@iczz8r z#GnTr^tNEz#d*^{w{8SX)S32bDB)qT;6tM$sPC-F*}S?F;BeZo>-tjkeDLNx{Ju}m zP;+B>PG{An0Eg3t&8~v@@7DcnS9$Q=mIiFLv0vtcPm^|Y=#iv$ntlFiD!H1c?>7O;k4n%olO}F z7=I7xISebn<*Cb9fYVbaK8IliJbmZMSU|VWEk1`K1#G`GKYo5^iWV6|#U;D=Jcbic z-?<0bBG=z`GZLWKXF(68x!bcQK8j8QC_*_O25U$-h zb)exeMo%4^{QpDy(h5^+tHtLsi~z0uZpt8|Z{Hxaac$CTLhDx{+WRW?ZfR>3pUW5m zO6MY`jQlyodo55F6;1itv4*QaEcthaSTG1>;amzxCqUg@nH=8mSEw~re;uB>vmCK6 zgUQFx?g|P>d;GLE{T5op`xwF1i~l*e;EGa29^L~JAV$! z0^9$GRy;d?SwlaB^OKE8*#eN)zg3T;`B=zu?Q4DHxT0V$84 zrUq!94g{CkvmR1#$^7_}kaqhf1x$4TD<=31V?T!4&0Q2=rvUL)sBj0!2chr?k`IG- zfZckOEPLqO5J6qpmWOs7&Dvy;(WFV*`{($Ot;@~U{9B*{_l3i~zr*j( z_xHW`oO>=j#{U$e)Nn2_4@7{>#?Vqf%GK{hqD0PH(MRPn77 z<%&KWY`_x%1eo(pm7#g51W*9cbsW)i3JOij43sG<9!6gUv;ZB$(9(-&JzA8ZNm96| zU0kkh4XH9)AjlS$2!#e2^!5U9+4Cleg>wY`n@~!X`PYhjn~#g=fz&|7Fo8fyNix64 zav=pmE0$F@WZ~1jA24{w_n-EUI5>5#sIqJ+wx?{oEgfv}b)%F*-PdwCrXEbRLo+Le ztvahnCHds}|!purjnt z4_Z}4OKR8b31~tiXat#Dn56&5ehy6@Vm3QlHEywx8tB{UcWs<+DH?5>J^fvxfWY5Ttq(`HR=ks^Eph>&(N3e zugzJ1PooMe7`t`?@NWNmgunU)>*fMRsJ*WIBkK`8d{(PHix7}rgoE!ucq;(8ugA%p z_!yPT6F7B}wv!(cFanJ52Aur@+QXonhKRb}gk<#9C2Bl5sZ|q|&C>b%1afmH>7fx; zE}Td2*@iH5%$YczffosVKZXnixwU5qOcT*sc}prmCVXXT(_N$S51ehG$D-uF`Hp1o zOPGKCixJz22>8M4#fZItH9JQt_8QBdk7C54U|9{9Ygkc)0LQ^uNFqW(gue~!vV|fg ztgPUqZXv^)!Q%m^=11>RwDDtzQXmAfyQ^rIlY}BE!$3y5@7WEaoM?ukqL0F_$VPK` cj7J0i0iaD6g5?oxDgXcg07*qoM6N<$g0{o7^8f$< diff --git a/assets/images/flags/mys.png b/assets/images/flags/mys.png index 022476291461fe068760cc472e5ea14c49b156cc..d151a3e4f14a96253cbbbcdea1417280c203c656 100644 GIT binary patch literal 1436 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 diff --git a/assets/images/flags/nga.png b/assets/images/flags/nga.png index ebfd82449d575856a0f2622a4cb1dacd2a8c8107..26efc073e8c39b2247fd0571e654bcec0bbd0ef1 100644 GIT binary patch literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|TR= zr;B4q#hkad5Aq%g;Bh%PWo!G|iC(&{>l8{DwkssKEn*OHoRq)+F;G7cH0+<`zTVot z_?^3bd40V2gA&F+OmQE@L+TwL%RNw&F@N|~R(iYbNWTND9+QTYJ`G zb?4Su7G=)5&yN+oyVbXCUfk=Ki}?jITo!EPlI*-B&D6_f;Nn{1``2&1HTp8K}MJFb_`ugGj|NlU-HHv!^fD~&O!RVdW437PqN=TC$;DyMmM$ eb*F6Fz`$_p497z6(Qse`+-^u_bk{WY4+gTWWjw* z9os+bXxJloocq-5MCLqu$%onv73hlW`Q&I_)&; zlimohLpA^%k`Hvqju?k~9Zy-Gp54H>dw$OcF^4^9ir&lf!+gxZAo0J`zE*-kVq(xC PageB|tDnm{r-UW|Hs^*> delta 753 zcmV&-hc4qt#;ubQgwA*b5wNgqxf2NaA98>==unzjsXRfQ|(P$*b<8jz( zwN$6m!5X1S6a?MV>6E&=yK^s>i}Zb8=JR=w<{yNdN~P3lwGw|qe2$j?e^t-(JVkO? zDwP7NgTY|nODWmJg4nzB`8@sc`|7I{@|Cav#QNL)%NMV6xtvdy4L$0U%jHs@&u8#{ z`r8@)FQgRD7K?>TE)!5w!Fk!_nM}qNmVsQt(Xt~f2Lc;)L==G{ECYRdSQWfvL==%O zECZ!H%}IIYz}qjMh4k!je^urC;?w(gs$4E}`-bR~YDL%tO%mWwuN~HA@E!|?wK15? zxQX(pjll%tndylIIuKZAQQ(@MIG9*4Qh_x<9;H(l^Wwc;56pgS==5U8X5|MV8&Ahh zWKrPFhkIMjB#h@ovfJ&lZ1rA`0H<>@%S=?WwhajI8;gRqjd>J1f0JoZcFS}+Z5>gQ zleTC$nM?v={2=VX1Km0T`t-?{yX|)C;$)3W7A|;t7G(4#t!Q4(Hk*wq6bip`Uu=!I zt_iJ+`L64#{eJIPt5yF>G>oDiR_*nAXG*Xqns6XT<{E_-K9b5FHHz7P)!l2EE@NEx z`~Aa_pmlYDF6v69N5X5NMqBF=$rJhJaW

z)4)M%SnhH4C0Rlx_y0+aN=W0j|{QL j3r9TiAOXFkhX;+H^-j9ERLtq!U1xgAGVNno({sQBk~}G0M}$F{EP7+gpx# zEe1TTiSat0BrS6CT3aj}wMxUU7)fnCbfISd6It$Kd}V*`xd637L9k&|k9N}CZ~7Cx z?Ry`84&VDczMaX0^#kjig{-Y&Q_>m~ZFnBO6MG^hwQgZ1|Ar3U?uhN2la-o8HePD# zC~TJ49_)PbV!dOZocV=sKN;8BE##RQHb0~~FYbK*Q2+0-6UFF$8blXu!OSDP;bkFRWM8m9{SUm2#(uE#gR7 zR6t#E(JE7!q&s2rZL(gNHHwu?H7u;zSYx=S?|itlw$90-Z~n4|@eUdeS&nH3E;Q{@ zd?O~(R7IRy6#`cbzEr*r1O;XBmY!=xl{CLGe#~x|(0JD_~fv$b>0_fU0hiX5A slvzwQ^H}D6;EySF5`l#d149Sr4~DvAOW#**OR7Mkp00i_>zopr0RAPTS^xk5 delta 890 zcmV-=1BLvk1A+%3iBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP0009QNklwkNf;#a{XarBux+kPl4FhuPin zy&*H*Ecw_bLAF1bytgy&z5Mpgf9!k#G=sG&>2z8n$vJ23Y+4k}Y2U?Hb4bl)tB45@ z87wT7O0iO@#HOdGVRLg+w2@vY6xiY6A$PqPOiWCO7$YMiT-sME6&4DGxNIjss+*mi zW!>f77fu(Pvc&&|#8rKKf`VLOQuLA_S11%`%(^m@I{Ow(k;f5XE>TKpmD zfk1%g^Lgk5w~y4l+8KtyQMY5Un1#~X*w`=`V~ih|*UMK0_oDQJf5CgOR-2 z)mfjq(zZ<-8}k&yD~`W@vBTl8(3^;c7hgg@ICp0Oe-~g-h|bK=d@giBlm;wLx-kFX zwdWvPyO|Sx@7Jk(dx0P-2kfi&-=G&1OO>8|JKbdX-VNBAM&!&=0Kr{k~%~A$(r#iRI4Px1bF*frzS|x!+JM z7P&K{g0&wvCDCYpQMx}J_1;4|RVWJ_IS6c1qb!NGxL1OEv=0dc`IoP}LaWO>u! z4yBRJW({2TH5v-etsm*_hKKWTM&pXQjC6Mlx2+S>mi%>H=jC$QOe7L?ULvBlT0)Vi zjg@swcMTmXcytw#3I?vCNtq-SXMMxAEOp$L3AwhmR;ePSP^~OvOLBaCT*M+t&(ao2 z)<#E1t4Bvib>ak#<8mvg3j_D1fqZUUNNDc1R+kK}%L@(KTu6{ztBVJTf8aCZ8PR=& Qy8r+H07*qoM6N<$f-xhZO8@`> diff --git a/assets/images/flags/nzl.png b/assets/images/flags/nzl.png index 11c6ade9cd644d01e7dbaf97538302ba811d55cc..306fdc77864f157879ae836d48175cc4b9377a95 100644 GIT binary patch literal 2096 zcmV-02+#M4P)k7RCt{2TnSSYR}l73B_1W`b4BnlqfAYf$KgIyL_c43#jI`!T%!_2~-%pSV^Rdtoc&di(X zuV43jU%ztJECd39Kp+qZ1OkCTAP@)y0)apv5C{Y!qF_}hK!~?JI!HNK0}NZ>m2r@N z{)xyqc>5fNEr7&~Drjxjf+7DlC}J4;9AEAaNApO4B6=?9f6WE3T6w40>~{G3@q1Xj z>2Xj9h}~EOdH1`()Y1y-gm`+tV%{7WxpFB22wZ!FY3%Xf|lHM)>Y@LqG}m zHs=MXdIw65`g+8&4F=6ni+$B%(crxOuT`=TG# zC6!7_z?#HTD63I`v8obOtCrJylnWPt;qDy(o6WBj_CXSmol_4g^%#tmmVk2cLWd)k zErQX)N#_z6MlEpeYBNNyE}Rho(Q69f!u1x=%M>tpIE%kto4F6HYPD$0x(4;fPXM{- zs|`hPBfk?&14A%$Du>Pl>XiNIU{#8XbZ3tac4b!kML^<2ksW<-pD(hiH7mhXTO%4n zr#HiyOHCnnUeE~OY}?)nc-^dp@s|zIzhSMT7)H;9k!!zz-D2S#WVPDi&Y#`1czQzs zDdxZb(*vgVcF-hlcU<$uXE6NZ8L*Fy^4Hq!0FR!^VENYKpgU^*>M$UHdy$;;uVGL< zLLE<5Bd&_HvVY7Y0of<(p}%hkbQdr1MOM9S3z*wlMbDBeMCa-d)74o} z2>^-Uih!Ad2?7BHB{F`zo4>t90)z;(^H**w2Gh9Ju>t^qA>BAET>mI|Gr&1=!_63p z96|Ok2TweQAAWuV*Kc>g{eu6XvQ7!_di8v*GPkzyOYQPnCEP1`2iNjC;N*oju@Z@$Ws9bAtZueV-ndZ1yb2BLS>`j)_@Y=%@#FJ7|)3f~oEqEyUUb z8I%B(W-Rm@F)AZyjG~A%x3x_+O_(U3W`J3E?*az&ck-ZLWdNec1}f5BSyE=Tcr`weIsu}r302!Dp(S)!YjUqiqlbQ@;UELlDcvjypX2E6??W)!BaWLN0 z2o<$T$#)9aiyHT2ho5M(kuDDr)Jec zN7pd@E&oCHl=~VQebbD*CSXT;CA4%5O?6UsmdHsuxrS=>+y{C?05Kw6-^lL-v&A-* zTy}+n+}Id8JDOUxR2rWN0VGo09WH2U8>Fi5bVQK=Ait3;;7T)EyR%dp0*GS>(_|V7 z<|J(ma!nKUmN*!1d;?F)`{>qPIDoeV2#IcL8+4!EcJBh?+b+nRfiB}NQi;=Jy-;7B zuX$M9%fGX?@8*#Jp{7qT-qH-}t(*B~jQ&a6zPp-L{@pENc)_EXpv2br#)J7{%3vCQ=g(mDgu}ZBRTtV zd7oGJOBtzw(u_P3;H!he%~ByLjPv zoOF}_R}T_!ZW?uy9CP%fZhA7}_~U0^xm*epU99@K3JG{p;fU0iPU`0FLp9LOshe&h zRZ7h$B2!Rft7~>osRame}DwA()15Jb-IuMHt_ueDK=%8f>dd2S@<>VHg%+ z76}M>6f!siLhlF$Q&8xgfi-}QJi{W)hB*NU1OkCTAP@)y0)apv5C{YUfj}S-2*j+9 a3I78RbNZD0LbmDv0000J7=CVY{pw5;m4&Vq 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 ZzX9uZNFJe!mcal3002ovPDHLkV1n|ltBn8v diff --git a/assets/images/flags/phl.png b/assets/images/flags/phl.png index b453f3933bef2d4dff687e33c06ef62e44e38e70..3c2ce7bf3940d32ad9f44dc13432b7cf8a8d1ef8 100644 GIT binary patch delta 1562 zcmV+#2Icvi37rffiBL{Q4GJ0x0000DNk~Le0001B0001B2nGNE0OFW;IFTV2e+AD; zL_t(|ob8%zY*SSj#|xSmrba=d5%i;elK27i6Re5~7(l(l7-k|4Au$38Bw(U`AsS{P zeqlyGEzym!Hx-uw-Npnr$A;+GWXbB-8w2OMb-lN5>t4Fj{>?e%)~@Z^b!&5Od!K*u zBzL#xEbH?-&vVcD-x&-f5{X12f00Ne5{X12kw_#Gi9{liNFVjN+t>^ z^*KlgKz6urdqlzUq7J;6M-Yr60I2cH0H#|$fUEyJ4|bgl05#4w4r0gdMgk$FcpI8b4BoQg zlP~@t2$CQGYLL}!5(|KZ-mPwfXaIV*x((?9;J)z#{8g)YxO#tD(3}w+THR>_z!I-+ zS^d>(lA7DvK8*bRzY0%IW&luR$aY86{n_&yLIBux)I2ta&DFWpe{EO_fQjp0BXDX3 z0u?La8mOD|nXBIv0>G~4d^TW0p!{Jzm;H`JR(HXnX5op_9sot=2~?BpZG0nU6~tQo zY;4F=Dq8{BuISe4HY^%|-N9FWcp?>fG^z?BS3euW#)jrqtcuyfWSv;uOKL%^m3SLp z4^@J@y9A0n4o}CIfAH0=N1!Z+2fOxkoZz3aF)A=t36ZT!P7qeviP9f?vc~FO5`gek z?e6{_o@<5h)jS(o-I3v|S6H3_Sr_W3!_5w=r` zePVXPG299FjcWL-9^=*H>aBs@VxHIP-u`Z5b^{1M;qpV)T@Zn?`xgv=ja3sLe%~MC z#5~n@Bx3$Dc>X#W*XsVbFmtLw41m)QEEs^y{}EtwXw3BG{c&5B{boOAj^zUz4H*$h z17K|;EiI^ee+^u{XJRIG)|87oXP$IqCYzdvvC-RDXX*!GC+|fv%)(duLa^;9 zHhyAWiWMO%j^L_)B}aJj!~$4=8OQ#IH2gswtQN~07d06(&e3%hzJ2S3Cru*&lsT^E z{C4!lsQb*o`yX6RLOWWv$(Stu41rDe2~U`I0H`ly%Ge8;RZ*>kt^UvpnTRUj=Z1=v zeD1{ce{bTi`p!1^3Z4_5C`|%TD`C;EB;%}n#cIV=(P6-NkMJaE8$cRf$~&)Iz@1&s z2v1H%15i)Q@lLo=tA1?k?y6rdJTVy$AZcKieujVNQ^FIXI{?bR|6sbX0LWb?JQ=zJ z;BKsj|IO9HlK{N|$TZ6sS_3fA{oUeMeFj>_f6x*D`)EJ65}Y?LCb{Z+;XS%d_!mZ| z0&e1xlBPy&gP}*fKtlEBs3$Yg&+E z14{1=0dNer>%uaIRsc9|U%_nMt8n$5#muFAxc@#Kz3RJuhi~sX;a?0{SpkveS@_Rc zaBLYvGXUlZXGd#RTgK2T0L40{3(FW<1fVC&7+M0LN6Q#m0l+QeyeEbK(a;+Jy<5f* z3BYYCgMY{4!v9d{6#$7uB9TZW5{X12kw_#Gi9{liNF)-8WXLA{2U9wDinX7AUjP6A M07*qoM6N<$f)kd|hX4Qo delta 1173 zcmV;G1Zw-844VldiBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP000CqNklofkO~vf-nSetSBf$pEd>OgKhU{WskZC(LPv9rA0y0 z7KUBhMLVkfpC-+cdyn6Dl5@DZf2PeIn)C-J-?`_0=j4~~{LaaD4p0tJsSFGZSZt+~ zVmF@(#q#=B>1%ncHA^m>grNs zG&eU(?VrtNMK~Om`aSpKw%*=e$>VT4w|U3a^<*-M>FH^=zrSCOjEwLYf3BA(6SQ-= zT(F^`!OrLNqEIM^Xf(>C)faL-7!1nQ)m7{Te;-}HSHD0YAgS)Qw6r*6oyp0`f)GN5 z7{uOArBb1xuW!tJJUG)i^zA$j9&(}Y(ACGNQWzZ_b(A(J^w#$Fw$=S`Q|DLbBe?a= zb2!=^$H78$((KtRLw>S!e=}*y#@*YHx8FwMfkyl~`~n8we;SDgBRD81P9<1nZ$j;` zx~>CulX^V!;v=|q{Uuy{{V?LuAPxe&2-zBWP(=73kf= z5am_AYY%5rNnr58Gb+OM(r+vlGn_2!K~8CQ=MT84bEY}b=G0BUf9Z7EMKz-G&L?m; zzk}Mrg6&1p|6YY8%kb*AR)pj6czHy%V7(CczX9QFz?nNm1iyqc`xJS{A;O2t{8+y~ zUVgIX`VH`h7OVzGj;CI3+$c&ww`YC^ND7D?gNU6dHd2J!@1DcpyO(j1NS{9cJ^q|s z!oDC-vFbsjmk2B>e-esV3F5wE)FTQJdA7`5G@VdeTf?<0_XEJn5UQaQpp-+65C@y)5+qyLywz%R)Gj+W}m>qTgU0Q{e}I8o^3<4wY9a(L8ixw zBm*%>+FPxMK7dRAS_mI8nN0Tg8KmC(2OOPC{m2{nU>n#`QR3&^`Au?}y(d>S`jd95akWiPC+uS;)46>F80@W0RB7g&T!hrCc` zpFdTz#ZnFYf6t^N${JNu&&Ldv<~{lYmOdS>s}6SqZA~sE&d@~p0OpR5k2~qf-xe3n zzSB|fDXs(;a*@t(4oy8LCbH~sM>ep$yd0pzmc>%=rhZ&EIjaxe!x`&JClmfL{Jvgj zU+!<)w#;NQg+wC3=OrfUn(T>)y40AbaObE)mX5BPfv zCnhGcI|S{k3;VL|=;%;mvE^s&OC)n`ZEZXA^YeM;gwk=j63m5x|I$D|mo6kM|GP?; n48F??3%guMuwSK%2aW##N(*w>#@c-p00000NkvXXu0mjfjH^P1 diff --git a/assets/images/flags/pol.png b/assets/images/flags/pol.png index 30d5a937142eed15a38ddce4a631634da569e945..1188eccd10748b7b8b9228c1868f93b71d716eff 100644 GIT binary patch literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|TRi zr;B4q#hkZS4|cLR@;G1YS|wcYZ>EHg^WF5t5+-Zzv#Tncyk-g14FLzVs_yacjsNVx zFj;p7gB!!@_&XaJ*E3ww%@LnqzG&})J(6Y#-%fJ$Gd!Y-$oYQug4aJUd^Y803AowB zxqxNEucNZkA2<~59(v96$t^}-BfpJzMXk{x)rJc0_1q_<3&BoU0Cd81pcD4!DdZpd z+Vm;yM#C+2=C@qeQI_hbJqNx0#1c{Xa;?K%n0)<1H*IskL){+OMTqi Rw3GuR>gnp|vd$@?2>>=*i|zmb delta 698 zcmV;r0!97q0>cF%iBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP00075Nkl$V5Ka2w0~AykmtJu_ zJ6D1OZv6oN$N%vs+zPtXg)ky_si1;j+xDZTPHv~k?ddynmpKrtI$bG`l~lz*Q-1n4pjJI zVlWseAMJLVw0#f+vR2wm7$$Gtxf2EWkKA62b z8jb24@*`P-G)<4w>9h)N2&9LP^Z6`auNR{GCM*F*@$Gavxx8c&ek3ej71mm<<_gQe zOTy8*BP<6J8!{(~L=l#OryfxSFPRfXrVGo!B~NoAtEZmpYCg+W6YX}pxvq%n1#2_Z z9*YHQV=|d}gMv@G^Y@Qmf5ImCKYcRGlTyruYZJ2aB=VifCBEk88veFe%&Du5~!9PV5tZl5L*qO|Ts${BED%KG-Iq8U|`~5yi ziQa`JNJ+f&sUzX3|NC&q(&O6Y;h!}QSvcV7fzZ)c=F~i#-EKE(f3;d4wXfi#Xlp{- zV!Z3R^mshte!m}QMAJO$5moMt_?R2=fPfdWl4$Js$tDj-G_yameQ3SWXehZ_t%4^* z+v)~e$X>6fd_m$_+cL?J*XHwhJVxLovg2|Y@(TkhTpH-->_WmRzN3;$hQj5ABfhwh gfE|@wJZSs@bICd!5IYUJga7~l07*qoM6N<$g0#0tp8x;= diff --git a/assets/images/flags/prt.png b/assets/images/flags/prt.png index ff5a25fa9ddfb8672af0b9d1e1b0b4f2d32cd314..26867667933a2001c192be5f23275e4ac50e1320 100644 GIT binary patch literal 2611 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 VTnVtGkH+`& delta 1163 zcmV;61a$ke6p;xbiBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP000CgNkl!HbTO_I&e8?qL7P$7{9&!K)HE`jCaF3jt|A6opkOP84RLMOdZmxt>S&D^wNTQ&S zuB(VcM3N0Lc8q^4-&vriUb@TJ{zKkRFQrAAN);Bgbu(`R}SX^B6*Vfi#j7B?Ae<2vER4OB* zqobi}wQAIAHDi2yTu28$DEi3Ah<|u^h!>%=kJew-&ay0@dSh~OvQAlFUtg~oh7q_J zWFOkw+Y3Lw`FM+;m(eFyGr-;7F;tLs;UmYnLA932Wa<_balJ_|n*WF7s{%cEnX?z- zEY{`Q)D4WO5H1+nIhnZNe~?8e($WgCtv=`kwmKE#T{03ehz1 z+XI%L81<^z_LRk*bx>HL634(O7K;t7Tm5uYF{YvSnz~Z}yrm>me>{#9N!daypN7qP zmww1Ek!uPA!M1J5=Ybx0R0nJ?IM+zPI~yd!;v|7DzQLUzKLGCE57^W{0-#(MAA`6X9}!?eu#na&ef~Ya5#L*(^R#usrO2Z(Lb2qe<-7$d7NsSi8BrL)tA&s z?pSfk^YkkJ)IAK0SS;4^s1WUIM_T2P^AV#Ylk@Pt<>AUF9#)cy(@l1(E|TPq&zl>- zFrd%4!RZNoV&&InPECEx58uPnEe{!HDkZUbXDGS81$}_mBuD1v=6c5Hd3Y7)QNfMt z*WcLQ-ae&Te+yz%SoeD`qoAjYV@SoEx3^FG zItL249PDe>o!vKyVekBkSv+s%3I?d|<;vaFH62%{Z&=)6(KTbULVMwJgLQt+ zvhK$Vzno#_Njc}ROf9M2ZIa=nIWspb{3ChldRWWWDG}Z?_bPNprW>v{d%Q;NSk|LA zZBgfJPw(6+n`>)(?ecPdfs8p0i@78_FL5*Va+s)H0P~r68{Jr>gfloy{1yZ@wmB#> z^>Ui9NU2{~;o!VLu<;m+6jQHoM#uui3mp#0K*h&6O|*bIT^48>0#!d|k@~y05fmyw b@I?P7Tjj;L7R!FuA`sux)z4*}Q$iB}R>guj delta 696 zcmV;p0!RJd0>K3#iBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP00073NklH{vv;nhM%B|kf4bKX%He;R7gA&5D&4eGsj|9X0zmSi!1pa$(mVv zYz||ucV>jRg-mLlPDep%t>rzNe}txN`xoXaORAV{M9ctWsL58VCAZtHY&05TI2@WW z= zSL8+Y8j^1f^57Hgo=3ZQ!L#B4I#Zr-3^pev90y)P$cQKov2YB_z4g1@E@a(lqJ=`?f7YXXU|ohG$_LhEyua%SSAmQ8=Xxra%fdCldF)YNI1D(C z?)S)}PQOST<8ox}ED%rw#-q&IHEcE;ZF|(Pb}=Zym&x|1*?OcI!ZFa*bo;HR1T36P zCWxe;g(C=sex@AmLR$~A`X;#}=`R)wpOP(wqhN=A(C-}&J2}I-f8t;wzDC^E2HRr3 z=XrX+-v_l?4fzs?hS7yjL__L4leDiE?;nl? zw&ey}$ZEA})`G+{+Y-rBrBXT0X0tOmiEFx?gs5N;FBV5 diff --git a/assets/images/flags/rus.png b/assets/images/flags/rus.png index 2633dcbd057fd6d6a0839c5213d01d3c3f52384c..460c7b8138c69e826822f7a70548d73cefc95560 100644 GIT binary patch literal 371 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|TR~ zr;B4q#hkZS8GBg_d7Li_t?pWw(C%;TQl#M)7kh8I&w;W>hCs~_&=5A~x7gnKl?n~B zb!RZFV~ET%f1ulG}8_CD^PIE|K_Ac_Ktf?9S(hBfm7XUHW~JnTb6%ALEIc zG0}U7qBfmQJxHaLcxF@3Jlp;se{(%cdO6#Sm56xKmM`s{U$zFx0sv)RbWWTIxX8Pljy3;Y*Xf!g9 zYN94!U4|xV0@h`GyyFQ+e}Rkm<9KSf+vFPHJmb-T90r`n=lfe$#Vvc0*1x`!OZZm( zoaMwOvvw9pr~weoGIQ5(xm=X(QO(-Lqy+y=wnz2Wqs$=3z(0 zas=SQHC)PD53=fAamO+^91Z~&TOLBePW_-i3lFOfu?pI`{p4AA=J;%H<+t7X~VKX<(io7ZP56Och)*-Pza`sOW|0u(D1ao{Fa(e!t1o~gS$zQ{$sSC=)rS+A2uNq@ z{zYkTx49)neF%`3hvnJIbY>E`CE5N?fUF&obUv=v<;<;WW+GVJY<~jMf7yL?T;?8~ z%S-}`o9$1)?;%OzRlsIzy1_QG=!5N7AsK|;On1t6!lw9Id1_Z#}!+^ zpnuCLhb&+aKOdKiJ0gHvj?)o9a-7}?AURGiK_tiJ^9}$200000000000000e4HCZq XI;qe3)RpYl00000NkvXXu0mjf6!=J6 delta 1314 zcmV+-1>O4Y1*ZxjiBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP000EMNkl!0iIci_}Ze|HO;w0@JD^Xt98d;00`oZqkK9ALi`qs!;>nYh(7 zP08nDaGCV}hrDajwWb{3ESM0((51NDZe@FWTWM`=#njZ4c*b@(999+<7POonhNh+_ z(MM%vr6&D%c6Jn-&8Erc{2t%y=;+Y6AHL7`^!-YEW@ZMnv$L7*?rv>#e{_`l$ml^C z(40slELN-4oJ=N_R4S#EmzOiN_(HZV7K^sIxru|6`;N39)K67ajqFTSRaKhW^yK7Z zN>LOMVi3JK7z~z-KkN97(2vW`T$Q!(w4O!Z*;kNS{R`2rKF8MZ4g4P>>LssQagxlG z-|tVG$>3o!$K!Ezt@+r1f8IF~{~P{=U-047H*i+hqy5E~U_W;rndml>^S`5*q6j)Z zh{WEZkcX8sX7a#vQi$|~Y{*e^==}rqH;Y-~VQ4>3m@8X6eb^5yq|w}x&4so1CAcc^t)2^{Lr*m6{~aQBhQ>hEYIO+r-4g zmfq3x{3GbQcnq#ue-$IEmFQorK_Fg|x8vOkagnM;4WeIvhWPXpj-rl_J~?K$+v8N9 zQwM^wT(xEbE)&2EIkM^?;M%;M%aV4vqYIAGG8{$i?d=tm^*dS6mX;O^72Q%JP(84Z zU@s9g19Nma%EeThe=`dzl1Wb3gA`ax!*e~16PH5x?uT%;e?sIdX@!;v|3SxT&hAPx zOtL~smM8g*q7)n?N11}YS1ZudWhXcjdjF`yV9<$TOOAFH)ai6$etsTCfR`_nqvwJh zpqkmgSSF$<*B3jFOU){SiG3aA@V>0Q;|vA+UTGM-c~)k|`U}`x0rl zepZM~9|=+Ye+)c${T#XVEBXfra%BrgRommpu8A2myLfh=vWj}itv_4_?{qEvk^IBw zZaPJPFN@YcwgT_9ThuP2kdg~{5m7eU+S+hupjrTSJ|tl08-)jCMIsSxZf;K4s-)&c zN#}Iafq?w4Gu+)P0C{LTyNUxS<#0F@eMH63^ESgDf3X?AK*_WGv_t@rpMt$lxXFRWX$k@*9BiC0i>C5k%&1NkYi=|vH7w?xC z^xv%rc!6fq|M7RZfSwDL;J9-ue{io+s4L5 z(HFP;S^5%WqNb)M9tZ@I%!Iw|vXQa}2Dyg@^7Gz-gemu#(IG?5;e`o#4kXym=-@%> YA4O!x;nxv(wg3PC07*qoM6N<$f-ZoGN&o-= diff --git a/assets/images/flags/sau.png b/assets/images/flags/sau.png index 97951983a0a89eab9edc06ebfd093923f0f0baf2..255dabeddbe0895439e11dc9ba0daae4bcf517c5 100644 GIT binary patch literal 2201 zcmZ{mX*3iJ7srPfTZ2Z~WirZR&(1Sh!_1IvY+1__jpB{66WJI;0001lM3|yZc=LC- zfhTn_KTP)oJb?(8AOL_*=y%uvg&0u)fZGyjYU~h>SubSTJ352=n3nm~+5HnrNYB() z>BtGurE95b&_eIBWF&&-9c0YO-VbhtEP`zVIn7QbrNpzH11T80+d9mP%^RsLrtrz^ zPE^*g`tPIc=^sVLLjbM)dc(jzS;3wYd==e;SzZ;pS!0f8a@Me4m7pcKk6~ z`!ajy#-I6T7PQ#eT^Hk(xHu%(?nRaM$-Ixg8h$N)Ugya&AYcJx+2aX)AEL^*&e0aY zoq^0`eXf^W0CjpHXZ$$YHXaunrNvU2Jdvl7F;_cMADZ+~__H%A1tx-^DAHkNyRv#vb(hVG@SIU>yuvXU-m>l z^@r8F%x)5&L(%;Gy9wuLPS-@fMT8fDn?fEHh`0I9Js{1(M}OVx{FwcH=Sm9u;S+LWv?yu70Gl7lNDoOw9$*sPZ{Dp2KgvpkHERQS|KApksp<}OU*R=dNp(u;V z(i$42?ViCY=g9y^DA)=yUa3LME`C*<=Q;W=Z$L*yUd>Ld1VP$XC9Lnd{oE$=$|R#p z?%m1!+9Zfnyf(&cFKR6RRrxSlg$bl=!PtK_tS~WJ_R*hh2F38n!nYQgUxq*KF(eN= z9~Kxmm}If4l%lm(qiaHmmTNfm39UsjtHBjLAf>nvI)~fYznG?Eu*7sQM$E{O_$Rb7 z4QekK0=VAm={`Ms!6pVCtI_eqP+3Sgd=mN=Zh!~MuBj>UWL^QNeZ##e_l+u^D4DIB zwQ)4v2<%5SNsVlDlBmykYcKei0{3X`+Xu*<-Nx^Jj@`O&S=$tA!n54f)>=Ztn;mf; z_g9R^w(n#CX?-v?{(YDN-TsE=OKR-c*3ltrUuV?hL2s12&}bA`!w*!XZ4^TKFyB1R zxB`X~Z*H(i4Er*fZV46o)JP_Cbv#GY0!3B|zMVAR7UF@wWJDO~c&PIZ8k;5PA)Phm z43yh(TF-EkEH#uYFcKd2yn@5YJ;-fRnssi*bbN*Amlco8rV^(mznAWP?;Nj)#MaC9 z?@7vVS)ntfFKZI5FD9mXt*unsb{QD36o<~_beYQ133{1}c`QKsnlH07D#LkIK-%C? z%Bm1CxE=H~gumGUmDn1Djl|M|r$-eg{kX7LY~+xbNS$XUr^M}5*RIb;)|AE@NgkpV zdI>`X^k^9kgBS9Oi<-1wgTPd+irNF$+;f8htEukWjG+Vme3?q|~nLDlwx~nV8wIstQe$mUuf>|6=$mgpAF_Zlp9@lN(*Qdmt z{b`;nS}MsKK0um(+OQ=2SW(OrSfp$lRc6i;q#foDOzvD83qH<)kP}Q1lg23TH}>wi zs%m_23$t}#8t-eNto0w`ToZ@^9W?&hj{;doS3w6o{;z>kF=2~%*WpIOfC4gad#~eD)>!;^B{ba>r9E2Lp>sqAdbjuIJ8vg?^bG{a_wD z=eg3r9Y_*9pAFed^U`}S5&ki&ewsV*`~RJva;(Sx4{`t3=1(AxdJF-NYUx$;oV;x( PzXkwlW^Gz)ax?xve&0M} literal 476 zcmV<20VDp2P)DtwcK0E?P>o~=kdA_r#@JNJJ;@jIVSMu*nR9Bbl7f00R z`$?ZFE^5Xj+sJMg0|>{SsqLJO|EyEblt#o*ShitB}q8otqm;`yaep)e{}6Y~iGL5n+{B z`m@2IWtJn^iM!v1M|c1*G!^h}Y7hX^L^pl&V@$69WVosg_kRV$izu~iwVx^IXrjR; zERf>W=22e7CEb`8t2c0bdP1z;;Csf@5|C(iPjw*Ws(3V4uZaemMg9)Lo-dp?E2+6d&rPMX`h`L5X+=B~)5b zS_5gcU<0PLMZDBv1kv77s>B;!&}h7%i4U`TcJ`9ZuI~QJoHemCW8+F9-|o!+2Ok+S zXNH;o|D5lebIt|=gb+dqA%qY@e+VIj5JCtcg!l>s17HMRmiNE zSPckOBD1m<$+jI(Xa7R9y9r(IKBf&9#I$h|)a>f%58IA1$fT#)iKV9LudPaSske5(cwb)A)}BE3C@ z6m{b&vdi8U7o;o=NH>0J7hjo3eY>Hk2Z*=-fXLn3=xb^a7o#i<$WMP2kvrY?g4^At z|L<&@oBg`|+He)pTNmp3e_;K^GunGGhHXIqx(|!${Z@@g{Iu)29xncCtGGZVZ$SKL ztIL3gi{E_GCRB-3^E$*%pF-q8FU%7MkX`=HkoQb)`55N!NAl`hJ*cA-kdI{(n;(_btt>cJ8RTXg%x{%^!ZDro}-zGD8HfB&6r@-?FoKe88@ zHFJ<^USFI62bz(opM}KFP2wVzwgEl&?FQVs;WogUgjH7*j;D@#_JdVd3{Kp1`^VRM zF-9N-ow28&XiTRZ=@)}v42b{oy}Jl6d27gPa`Pq*>pVOZ!#1FQ?Fa79(3Xuun|Az1 zccFe9kLJID}CSH-6$C1-myrwQXwmW_{jTHFcR8u0r(sWqm%g`aN-R z%4;#s>e-I@K&F1Ct8KD%n_hmlzVn@5H@UeJkZjwDOv7ySe|=RaE>8Ivp!WjCLY;Fg zNB8z1vtqivUAU@nZ0&|Q>Og+roa5NbF=7T3Oc;asv3>3hnWl!~zW;;%ixD+oa7%Li ze5fnu?S;AP3esPGrqhh{OT*I!3`D3B*%i}}+_xRE&Oh~L{NSei;qUZ0ZpsJvyC8UW z1Ta7Yf~DUPe>ehyC2oLohRZNVR0A5IWIGiNP&B~TX)VASVA25TUTAOx$O~aG1<8xS z8le0aEW(Ngi1$K+BS2mVgDFT}4AubUzhDtoG(fx;8XN)gLKsXz@?x+CDE|eEu%ZFt zz0lwYkQc&W3X&ItH9+|*n@~Im)mBf9f}zw>5d!(qr1^2P zJHBtstdljVy)^6x!+SIPX7c5mf48$UL(~n?tE{Z7Sg=w`(J!V)vCR1Y^tB9Yg<=C@ z3LwKpEG{mJYPBln=H_T?YfC+YUoMx$&d!bu{V|%Jo>pUwjEqPf-)J;MGMSY6Ir^b( zVPQdH9JHg&99R3>+uM}O<@}|kCAqe?hB17TCBp17D=xj0a3i=+LyIj5DX;`9y}!ZcO|2P z)kjL%<3=Wv34XNa)FU5J;up^U31W2fe|qO0+2_tEV)AE| z+;<;Td+Wt3t{=ae!0_;Jx1u^>O&}kCT)wH)OD^9I$U$kW1cL_F(I8>N04d~W6yTVZ zZ0;>Z3iI>-E>Bnrq6j_Pfuf_MqZBnjym&mW6m=-VfOx#V2MfNW2t)62_0Br7z=j%7 zifYzTL#0xYrlN|qf4M1%3h-qz6;)G@V+KVGyfk%x>-hlY=ks}wPk(z95%|8p$7eW? zrygWw*Wr$>Qz#T1K5SV?1#{~Mzh8Qohco1r-b{qY2*;YxvFLBxwyf9d?Ww6LoR>h< z557!H)aOQ*p6KX72 z)O9SAY>kbLHFtM+U2vk#ak&?Q3xn{bfv$HhB&_hWUY87^%L|M8Tu30U*TsXzZ^Tmp U`=k5dkN^Mx07*qoM6N<$f~h5^zW@LL diff --git a/assets/images/flags/usa.png b/assets/images/flags/usa.png index a8c44ce75b7960e2d1cd9cd820ebba0ad23c2334..30fc880b74cb7775b913419e21d038dd5c9ec206 100644 GIT binary patch literal 597 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|OJg zr;B4q#hkY@HhM8T3bZ};Ub;6)+uKokV~9)020=%`8ix4?qBe4wEfWybF%cB;77TRR zC@98vGR!IFW`iQ9RPMQ%cWUH$wwK>-`G5cBOwXlEK%0<&T8Cj@s_<6}F2R^m6=r4` zwh4^(2Sr1!mb1QnbC1t-Qh7QOoo9vi zw707j$$$6hT^vw;8PAMa)-Q!KioPxJZ&>u5zkmMj>P*`T)~WXzLKwbheV1K$$#^fn z*a@ZY*G0V`Ku2Ohr5s0?k;7{nfi)5f zN`Y*i3s(Fr+H4uT>lZ9d;XA>5hVj;~|7U{OGT!_*=4Q)y2P1(>-u^e%&RucspZ(^f zhHo4|!x_}|H6$NAaec~Fq`HdnQtmAM$^?f##z)I9OlJJ#RIyH>(r^Xi&b>dja(Hjg zT==Ask&pR^f1(2OlU9xx$4^NC%yafWSl2Ux6 delta 1215 zcmV;w1VH=M1jGp;iBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP000D8Nkl(GPN$^|`ID0qEtkvL9UY_b@o{Nm zaB$F8@s?$2KA+E4<){Z=JRY~v4*c*r+g16UogLcU-EAZi2|JlgqK$@=e<%nVl}g3a z*VkuMt5vO5t7!uR10XFwh`h(+u?vL){Tn*#sQmwpGfmUxd?OSJ)!EiJH#cjVrisMF z?2Y~XeQz*0`-JSqXt8K-$zqVHt01gPcGv3 zdtv`|wzZX&mAc6pNUyVsf4ZTcjZRHPgg}3Plc>%81QC$5ZKZ~fB24t?s32uXBX8ZL zS4M;6B1Q{K-xxfKb$po0%X7+r&1Mg1=8XwTtv(jAAPC>&WQ11P2cR>nzdja%APZ8(m?{M&CNG`P#KF0K9p~7XQeI_eVOY`xjgiN zUw&$BpJrzv2th@te}qK?63Y)aT0RW@ojbQ^Y3XP2fh;-{PN!TS`ckRX@caGUi9#tT zrdB`5sbPnr7McK97ax*&ct*a^Ai0TPC9kzDCQMk~bj1N;ckjMKi;LgOiU(nt z3cM3wkzpQ7XP6``zR{s~KmCAScX{Y37HehY|CIl}crBSee~%gc3-b{}4T4zpAmUUM zbJXdB!syWTKXWu`x;ykA=e7f^sKtNHwYH%Lxg#bA3xEI;<|TZxv5z$USyoZS0ik{m zQRM|}Pshi{wD|Q$)KlQ0SBIMx(V?Ls`t7|Bsh0rpyk2j+qJVYq2lWyl9?#L1qOD`t zn~#2^UczTQfAs2%E2k$_u=UC3)N4=&cu^FYXf#TNjWqQVPESwmOeQ0FQIlDFUSgzD zslTa*5R1jy_E8~=Yewd)`_xO|t*OD+5y{J{=L=Y4ZEdZdS-k&DBP~ol0GsS*l}p|_ ztBK2<-FCTSYv%KLlmGX1oKJD|Qx%biuUn(e%NgcXe=rzq?tL9?bjCWNVo~2P47*$| z*T%-ia9;w^hOPpcXoHKFoc@d0vdZ(ZOzislx^>P_u__@J zHXII1Td??6u}rcuJUo29x3^aXC%U*UcSGyJp!3l{eP29C=$&QV9vM0wFLZj&g9PHb dJv=D<6$8r>xt=s2$20%{002ovPDHLkV1nqqMVtTt From ec8c404086f0c87e000afb881e9c4ba600e5602b Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 17 Aug 2024 04:25:10 +0200 Subject: [PATCH 039/142] fix rosseta builds (#1593) --- scripts/macos/build_monero_all.sh | 11 +++++------ scripts/prepare_moneroc.sh | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/macos/build_monero_all.sh b/scripts/macos/build_monero_all.sh index 9f6130066..c934b4667 100755 --- a/scripts/macos/build_monero_all.sh +++ b/scripts/macos/build_monero_all.sh @@ -43,15 +43,14 @@ else WOWNERO_LIBS=" -arch ${ARCH} ${WOWNEROC_RELEASE_DIR}/${HOST}_libwallet2_api_c.dylib" if [[ ! $(uname -m) == $ARCH ]]; then - PRC="arch -${ARCH}" + PRC="arch -${ARCH}" fi - pushd ../monero_c + pushd ../monero_c $PRC ./build_single.sh ${COIN} ${HOST} $NPROC - unxz -f ./release/${COIN}/${HOST}_libwallet2_api_c.dylib.xz - - popd - done + unxz -f ./release/${COIN}/${HOST}_libwallet2_api_c.dylib.xz + popd + done done fi diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 2e53a54ea..94754c935 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,7 +8,7 @@ if [[ ! -d "monero_c" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch rewrite-wip cd monero_c - git checkout bcb328a4956105dc182afd0ce2e48fe263f5f20b + git checkout 5de323b1ba7387cf73973042f06383d4dbe619f5 git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero From eef319658a1934c15768b335c7164b491de403f0 Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 17 Aug 2024 04:30:48 +0200 Subject: [PATCH 040/142] Fix legacy seeds not being displayed when polyseed is not supported (#1608) * Fix legacy seeds not being displayed when polyseed is not supported * fallback to english on Japanese, it errors with empty errorString - probably some kind of normalization issue --- cw_monero/lib/api/wallet.dart | 13 ++++++++++++- cw_monero/pubspec.lock | 9 +++++---- cw_monero/pubspec.yaml | 5 ++++- cw_wownero/lib/api/wallet.dart | 13 ++++++++++++- cw_wownero/pubspec.lock | 9 +++++---- cw_wownero/pubspec.yaml | 5 ++++- lib/src/widgets/seed_language_picker.dart | 2 +- pubspec_base.yaml | 5 ++++- 8 files changed, 47 insertions(+), 14 deletions(-) diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 973a38535..1a6e5315d 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -45,12 +45,23 @@ String getSeed() { String getSeedLegacy(String? language) { var legacy = monero.Wallet_seed(wptr!, seedOffset: ''); + switch (language) { + case "Chinese (Traditional)": language = "Chinese (simplified)"; break; + case "Chinese (Simplified)": language = "Chinese (simplified)"; break; + case "Korean": language = "English"; break; + case "Czech": language = "English"; break; + case "Japanese": language = "English"; break; + } if (monero.Wallet_status(wptr!) != 0) { monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); legacy = monero.Wallet_seed(wptr!, seedOffset: ''); } if (monero.Wallet_status(wptr!) != 0) { - return monero.Wallet_errorString(wptr!); + final err = monero.Wallet_errorString(wptr!); + if (legacy.isNotEmpty) { + return "$err\n\n$legacy"; + } + return err; } return legacy; } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 07c3b8876..51efd4076 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -575,10 +575,11 @@ packages: polyseed: dependency: "direct main" description: - name: polyseed - sha256: edf28042e7b0b28f97a0469aa98e6e4015937cef6b9340cd6ad2822139c95217 - url: "https://pub.dev" - source: hosted + path: "." + ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f + resolved-ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f + url: "https://github.com/mrcyjanek/polyseed_dart" + source: git version: "0.0.5" pool: dependency: transitive diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index b5a13a126..7f5da2621 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -19,7 +19,10 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.18.0 encrypt: ^5.0.1 - polyseed: ^0.0.5 + polyseed: + git: + url: https://github.com/mrcyjanek/polyseed_dart + ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f cw_core: path: ../cw_core monero: diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 2ccd560ed..0a06a9950 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -47,12 +47,23 @@ String getSeed() { String getSeedLegacy(String? language) { var legacy = wownero.Wallet_seed(wptr!, seedOffset: ''); + switch (language) { + case "Chinese (Traditional)": language = "Chinese (simplified)"; break; + case "Chinese (Simplified)": language = "Chinese (simplified)"; break; + case "Korean": language = "English"; break; + case "Czech": language = "English"; break; + case "Japanese": language = "English"; break; + } if (wownero.Wallet_status(wptr!) != 0) { wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); legacy = wownero.Wallet_seed(wptr!, seedOffset: ''); } if (wownero.Wallet_status(wptr!) != 0) { - return wownero.Wallet_errorString(wptr!); + final err = wownero.Wallet_errorString(wptr!); + if (legacy.isNotEmpty) { + return "$err\n\n$legacy"; + } + return err; } return legacy; } diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 85d856b35..f164b3086 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -567,10 +567,11 @@ packages: polyseed: dependency: "direct main" description: - name: polyseed - sha256: edf28042e7b0b28f97a0469aa98e6e4015937cef6b9340cd6ad2822139c95217 - url: "https://pub.dev" - source: hosted + path: "." + ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f + resolved-ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f + url: "https://github.com/mrcyjanek/polyseed_dart" + source: git version: "0.0.5" pool: dependency: transitive diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 7a45eb628..eb14286a9 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -19,7 +19,10 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.18.0 encrypt: ^5.0.1 - polyseed: ^0.0.5 + polyseed: + git: + url: https://github.com/mrcyjanek/polyseed_dart + ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f cw_core: path: ../cw_core monero: diff --git a/lib/src/widgets/seed_language_picker.dart b/lib/src/widgets/seed_language_picker.dart index 3bb0376b1..5cb8f2fd3 100644 --- a/lib/src/widgets/seed_language_picker.dart +++ b/lib/src/widgets/seed_language_picker.dart @@ -17,7 +17,7 @@ class SeedLanguagePickerOption { final List seedLanguages = [ SeedLanguagePickerOption('English', S.current.seed_language_english, Image.asset('assets/images/flags/usa.png'), [SeedType.legacy, SeedType.polyseed]), - SeedLanguagePickerOption('Chinese (simplified)', S.current.seed_language_chinese, + SeedLanguagePickerOption('Chinese (Simplified)', S.current.seed_language_chinese, Image.asset('assets/images/flags/chn.png'), [SeedType.legacy, SeedType.polyseed]), SeedLanguagePickerOption('Chinese (Traditional)', S.current.seed_language_chinese_traditional, Image.asset('assets/images/flags/chn.png'), [SeedType.polyseed]), diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 567d1b210..a2f346bae 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -94,7 +94,10 @@ dependencies: # ref: main socks5_proxy: ^1.0.4 flutter_svg: ^2.0.9 - polyseed: ^0.0.5 + polyseed: + git: + url: https://github.com/mrcyjanek/polyseed_dart + ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f nostr_tools: ^1.0.9 solana: ^0.30.1 bitcoin_base: From e8a03fc00e22a6428996a5f4bf5ba1f127340bf1 Mon Sep 17 00:00:00 2001 From: tuxpizza Date: Sat, 17 Aug 2024 12:40:52 -0400 Subject: [PATCH 041/142] Update even more flags --- assets/images/flags/are.png | Bin 190 -> 376 bytes assets/images/flags/eur.png | Bin 1005 -> 1476 bytes assets/images/flags/swe.png | Bin 752 -> 390 bytes assets/images/flags/tha.png | Bin 774 -> 384 bytes assets/images/flags/tur.png | Bin 1037 -> 1088 bytes assets/images/flags/twn.png | Bin 373 -> 1005 bytes assets/images/flags/ven.png | Bin 1009 -> 887 bytes assets/images/flags/vnm.png | Bin 448 -> 882 bytes 8 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/images/flags/are.png b/assets/images/flags/are.png index 8eb1e2bd3815aa15b34deb6a323f0b55a87df21f..2df30486a8deca355ea0d7b2cacfe03f727fad2e 100644 GIT binary patch literal 376 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|TR$ zr;B4q#hkZS4`#YL@;F~ysdeVj`Z@n%ULN?M_&epN6oH z`}08<8IPo3v!wmmEp@*b8kRw{23pJINP?Td}%LotUvt+!d9^xlXI zxW^-X;d4q$G{Z-+Jn;$UN!5;(#yJk>CkwtyWSsMR+3yO4hS|C;4C@%&_Ag0gzQ^RT z{lkuiJ%Y!%Pt8tbeg`y5yP<-+pZkP#;hqJ1B+VY2n=H7GsRLb+wNAsTMO8Ke4yoLV z4MGQAOy82@z^QQeP!Z5sr@%&K&Fd-PRJcb_gTe~DWM4f#2b#! delta 177 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 Ur5XeJk^lez07*qoM6N<$f)!2PPyhe` diff --git a/assets/images/flags/swe.png b/assets/images/flags/swe.png index ef73086f6d3a0054edddd56f218f9f49a9908350..e5ee36d2feb051709c7cb0fcc6d9f303cbceba5e 100644 GIT binary patch delta 378 zcmeys+QzKd8Q|y6%O%Cdz`(%k>ERLtq!U1xgAGVNno({sQBk~}(ZbWkF{EP7+gqo3 z4?A$U2EJd>=(c=+W@Fn4tJxmYl!T8z_j;2oQP(fS4%7t!A2fo_ZxtRZO!f+}@U-%rrgo?AQ{Sgdp5Ao%r;M;5Xm~uof>Eif(~fN& QPaH_l)78&qol`;+0E=^y%m4rY delta 743 zcmV<1MmeQiBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP0007oNkled5UuVR6+!U@!rgd9 zvPhP}MFe!7kY(G_KqKVl3ArkJ(1VB!~<))^4}iZntC2 zW)nuEk$gw`X0u_l*-XU#7&ICU>7!IC31#m%4$EeeKxyeLNn+ zWHJdmosQ`BdgLS2i4sA}b=_1Zld(L{W4`aRd_GU4~3<#K>B7!HR%V@!q^kiE57EYe?|zP(4zw{QhMzq*4LHmx7 zY5*r-tGIYyCROY;AK-Qw(7IEw*2an+4$$^yfnbK+n)=p!T<5v4^8hGDf@ zfyy2cQTockFcb=fh?CJ%f4|@FL!}!{v{)=g5vBAb7c3@S2;aPh-$nc8{*T5d;rZxU zq|1n^1#2=MUV5TbU?S9P+p42dsQ~|YceR6mzt;miTD^mGI?c|Ang|LW=JzP!8~q`v zGp;&W8w)y66J$gcYtyjZZiOCE$=bw23GGaJMCH^|nE{3YQ`7lge~$%NIGs)d5aBm4 z1;_w#Nw-?9Js#)txksGfR4ykWx-f`e8mQ;fg#?ekOS)u;U0!gwa3Miqc=*Hv+rZ^y~)4<@0+!{H$O|j^mF7DVzyZxD29jI#8huNZ~`U!p%U1 zA`W|64|AW8Hk`9ykEB_`x5Q~<4Q16zBh&gjtR$Bawtqu$$32io`u-6}z# z3AceJY@7=;Vg8+lFa2j;pI2=V0t%l734;w@@arVUbfDfCknK-_w*Pnn3PULP>i>s9 W$i3~n=<1qzAR$jzKbLh*2~7aNMV3DR delta 765 zcmV0Icd?Rynx!zUF z`I5;wcjnCOx97|Z=M2#)jHuRVe>6-;A%t9|$>i+ic=we*FGvr3`}ipJn5ENdwOpGOxZY$h z^8T=NAqiY?^em2|iw{%v@}9CtGXy!=!GR|?SELm_+{l9o)hCKg{P6iD>4e$!Yf7b3 z@{swV$0;Yxz$AT6C!xRlfBc1X!g{^-5{ZP=Pl&#~eos0folfIEm%0qS+ORr95>-EU zI>RK*p`z^*oy}&XBUH3~qPbj-bOW6C$Au4Ey?I4C;r6zH`~7!T#mzrRm(QM(P5@Dr z$@fA0!sDc+-7cBJEn z9%Tk;2ELj+rOWGy3ovgu9J<{5oiqWsa1H0OH$hgM6n8A`>2zvyv1KAZ4W@q3(HFI7 zusFlMLNWn8yhhkI2xW`;mSu^}X5$o#Mbt|m>iHKw6ZN?B!0z5-UvTLvB^ochW0MUe zTCo3UyHPr)Wm2}=Q|;}Jp=?!yEo7lkkZVEWTiG(neLkPxjmP5yIMK0Oj)MPS5PmdJ v-^T|DGdvdc$PjwGFzKEL3D`wFJShAEs8qVcYmOlW00000NkvXXu0mjfi^^_d diff --git a/assets/images/flags/tur.png b/assets/images/flags/tur.png index 166c6313a131fbe928dda9d4811f343a79cf7a6d..e86b5a85e937ba903a7f14a47b000ec6f851ed47 100644 GIT binary patch literal 1088 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|WCX zPZ!6KiaBp*Wp?-_iX2~GtKOx?B_G(u?^Psux`#7)O=O$PH&gH40G?A)Z4+Z7?KpZ5 zo(THpRQA(gn}=~^+sut;Dvn3|v%32w>yGsmo5y?J@BJS?m;Z9^yzlq^|F?NwIj?wq zs-P2xVv7KR5YkA9PKcPgjFERksc;Y54HJb6s^=a_C0t~9tHYAsuumX?^OAUTc7yDm z2TD1Y8NQ+k{`lWruwCoG6}O=S4xbDZ4tk8im%6aptl;~i z?LQ)SH_kJ?DlfOsRH1nF_8-T$t+qF*$vyCgCI9357>24V0reR_t~K0v$}j!ub$+bYk0bN@9Fc86w~r+9{Z%*a^ zbnlFkjh3Au&40({^}py#k-e+M1DNyX7mK^@nlHNZ z4-Ga{@)>Mdy3668@d4c@IRY0-4|H)bGs%eYZpdgT6KgVdi09bA^0ND&=>fjD0>N#W q4OQ3$l_sS#pkzB>2Anqk2j|0D8OG|#dsBf$0)wZkpUXO@geCxvt^Aw- literal 1037 zcmV+o1oHcdP)EX>4Tx04R}tkv&MmP!xqvQ>8^J4rUM$%ut;yh>AFB6^c+H)C#RSn7s54nlvOS zE{=k0!NH%!s)LKOt`4q(Aov5~>f)s6A|>9}6k5c1;qgAsyXWxUeSlCeGu`eQ2Xw=+ zQi-^f$*xMFR|F7&h%kl~W}9=eoP=+E-BUN!U5sb>_x)MDTFz!bKq8)HrsWWC5KnJ9 zCg*)(krfq<_?&pmq6-o~a$WWKjdP*N0?!n!bZVYhB$f&ttadOfS~~F*aa7X{$`>*o zYn-<@>*XqI-IKpCk~3D8xlU^c2`pj>G9;*|qJ%OW#Ar82v5}(lxQ~CZ>6gi+lB)zp zjs;YpLuvZK|KRs*?fm3~mlTNu-7l{DF#?2kfkw@BzmHwFaRLONfh(i!uhfB=Ptt2` zEqVm>Z37qAZB5++E_Z-|CsVPMrWB+lk8{ps& z7%foty3f1AopbxQr!~JHC5&>2@ijKN00006VoOIv0RI600RN!9r;`8x010qNS#tmY z4c7nw4c7reD4Tcy00JFJL_t(o!|j&MYZFlv#eb8T)Mh#v6G2lKf)zhvY|($BLPa48 z*{k5kqMKr&ML|U3_h*hN0jSEFvu~0Em7ybbSMKnfqp@>l-am-hnnT(5pNknL3 zOsX}xtM}kO-r;c0y$4#=q$QMJ-pfAoUjnx&o3QaYRtux62B=iaBeBy*$B84j`g)PJ zZ3RG&zacjlB{MOJk@)=gH7bXKcq3y7iUNS~;UoFzJcU=UajaEw2K;32-Y0kOL0vWa zTa3z~AnR^k1wjDQL>n37dpLrbU;L@h>EDjCXBUO%FIJvWabqXGg{L^&ZU9ol=SW|^ zwv4DG&~=10ZJ4?NhOsiPS56+aK`t#kBOSTE`~zLLKJa=`hX=~25gKcz_P$>GoVhUt zX7wXi9U7qcO`~mhH~B~NWbe#0Zahg{o9%PqWvsHMI%Hf2_v0BK0a3&=dI3REY6AIx z4t)LXyOOJS?x@&rX0qg_XGxtp1BO9r=qzSBjT{O#R-^g3D9-MHWvpX|$xPj%~Um`>4`~-nr1P&rw66~cm)b94vNVn+NE$A)n;RX8lMs*`v;*|1i*9gJnHE| z1Xuenz0QE&mWR2xRMQOo;}z;UbO7&_aU7~zTA`1hpvU3}s@qzjx2MU?%+_^<)|5xZ z4ePB?vTrZamM#!Q^jMtS!>A>ZFJJ4PM_a+UX@hgCnznubZgRnjEZ(vm00000NkvXX Hu0mjfBSqfA diff --git a/assets/images/flags/twn.png b/assets/images/flags/twn.png index 4af8ba78d6a7e891800d67ee6be6c1eee9cd4883..34a2b37db2d786516482fa989fed3631412b64a8 100644 GIT binary patch literal 1005 zcmV_R;gf0pe^B_b%XelaMO$0r4p}dkF5tjV-aE@opF=sroN6-HU3&uVB z9ESDZ>vGo7vIrrB5JCtcgb+dqA%qY@2niIqN1n(*GEvrnG76w503%>0OsAAwrXc^F zRC=jh8wIfTzXn`6+4#@e{hFw@-R#X}1nkWry{!{cc=l<++R%&7GW7gY!rIl-P14!? zfdcp)0ij1D@^Si?E5iAswKDhnugw2h(VwwaaH>hBCw@r7!*>+GZvm>yBLW-|?v(V1 z!|jqD*ZH8TEJ6YN79cOXP8JtdwS#9Se@a`!m@7bA{g_?@*pruCM*;RCK>mqF>Fb!3 z*7{MoSJ5vQkJan-=-?Nz?RDvD8JEh^h+IA%(uU97;c@+4bTAfAQCYP*G9Ev8E6*C= z>wBj18zksY!q?6;%j{JAR4*;s+OY%uvvTWtSJG=Lin{fl=wM7=18@)Ai(OC}X~&z_ zak?Gn7hed=n^!SeUb3|zGHw=hxFXp0y6gO{of9J<4H2fszw5N)IDdX_MYggqzpB5( zo}TbT+9#hW$m0aGm&oHNxRp10^%_ooGVol0Jym59eGRU4T2uOXah1VHHIAH(q z>#thWROUt!^e!k_3PQn6hBKC^VhZv{0Bgj;4{Z+P!gZr= zs^TzipU2V86E4TmmYEka&Uk?@WUS15C5g7RldmNG7l1G2Ewe>I2 zEiNn{1`V*`9bTp1XqiZXP*WZ?P7z`)KvgNbRHgv5Mi=4m1#b1N&4$H(tsV3=ZU zy{@$M*!1aFu3ULrQgYPVdMyXX3_H8^4GpKheEIeN|Nqj`qaq9)q6{6H47INrxL+}F z|6yPR+A#Ii<8mM+UlQaO3{-{(Fr1Yv696jZEbxddW?liAS lxR#%7@O1TaS?83{1OP(Cm7xFt diff --git a/assets/images/flags/ven.png b/assets/images/flags/ven.png index fcc25ef2b301e4a653f2d467d7803d3e5b5e55cd..c189b05452fa2be94ea5dc3c09672d34b7e9bc25 100644 GIT binary patch delta 879 zcmey!{+&&+Gr-TCmrII^fq{Y7)59eQNGE_W2OE%lG^5;NqM~>`v!d2b_w?;P-g{%HxochV`+s#C9_;vTnZ0)NzP;af$vtloaNGcJP{kikvuWcYHt9}lNbu6r?g z-`e~7pO~IJIl5uX3c>Joc23#9V`Z$0WOugq*1v5&{{d!}2iFAa=sUTg;v1HINy*xi z^?m98qMQrtwW|Nqc#h4IzMajOm9@KY^?yk>cl#vQlbu_RC-WSuPx3kTEw16uqqLrV ze_1{~dC74~``psC8eh&zv=~a>u4d$ue|vP}uh|ubfAa2i9gOHXdE*}U1mox_bARil zr>;5ijiJs@<95lhH3FW=eD%M!O#I69Vg8cK>Fj4&Gu}(Me4VFP&l0g|6XRW5enUgU z){`gmcU%=S70Q}~^ delta 1002 zcmVB$7x`(=7o6S$$LBVe|Gxqn>Vj-2A~#Fy%vc?WRi?A=`fo*O}5+r=Uv$* zWrwXrjDyHvlfvP!l+Wj-@$qq3U0vm4q~~%uDVa<%$A`hdzyP14qoacf`%FMovNw~bu$nM7$}WcELN2yiK#S*?B#Si?OL9Dzk$^I z^*n&4X#j=}tpc*%y#6RWI5jmzA7Btsa=naRl!@flt_r`B!w}E(z}V#vaH%^3tpF}{ zKdzpFt7y=(ECw*96q3)De*+Nul7a3|Yw$AW2EC#}s~|WLWanVNT7akd+weH^0HU!V ztfdraC1?|pw1`p=le`x2!`jyb_$tdV)G2{>+6Aq`&dv^q>|w1d?Q)VX?+>^;(g|gE z0xq7?;7V@+S_Qw~Z>7(g0DQ4D!le;VKrfC$LMuRG=`6Ggg+jsfe|o)KuOWK>`7LM$ zqeI<1uju(b(9WEOj~hRs8C(s%rF_nHPLu)`lP;VT_wJ0r=2njD`tCl-(cyCtI^OLmX$7bAGgfHniBCR6C#qj&%_e-nv>VKQi&-9%jQ3}@id z6IoVsxI?LKZ*Qykzb{iLh|-Th`eET=Kb%oq(UOtwoWp5rhp?sh6-8miVzC+ug{skL zl!%&E!bhSec2>~cWy)Q6bmfv7PE7xgMUvF){Rg&1p<`Lb<;u!RX%8W6MI&31eSLj= zE|T;tY>{NSK&z{3FO^DFh!Y$fm+Qg0FmPTPi06X~3E4ST?~=iBc_G6g7ZPMw@8Ut= YFC`2cH_eDwQ~&?~07*qoM6N<$f*IY) zr;B4q#hkaZtn-B(MOy#%A6x%5IoxTHV*`tagR`R}BP&azz#j%F4TerNUR{oiAeRdl z8d+Q?Ff82YDdYTzz0$(XkW;Db$#K`+7jHE@?yxW{PkUZ-Y2}*O*!#xC>(Z|;6aZR{ z0^Af^6+C*@up2mh&@+%wNNBhyD1L}Xz@VX8)awnyKQ|8-fqG*D=9bDs0%8UYwP=D| zR|Nh&X|XN5p16kbsdS-MeE((;^YvuUsM$UnuO}{Hycsg%7B^q1Sk9?sKVKW3Yq)e( zBJk?RkX=H_>yKM7UsIbD%e>ED<>qU{Qw?iWSHy1H&9-H~)fC?k*He2Kk9zO@@iBDI z3U?mXLk+hs$Nl-S%J!mi+ZJ;LVeu=oULWmWA9iq|Pt0HEncvt%^jv<=dcV5B?o?d- z1Kx<*EAsaH!sOL%yuJ*y^QpwvXTOzGuVjhmPCG7fN>ZV9!JUN$e`}`x%RhU+V)C*( z7dgrr{+w;`2?=;Q&ox9c@pHJ$uWt$ud5c^wG~6|)*Yi3%^Kbu>w0mJm9@>(QpY(m@ zIWGVDd}W{M|Kr^vzVacD)mMqJd4wlAcAM+I(&5`LZFRa&Jlkl|f$ta3F>G9Ve)-}n zKQ{bk+&gpQ%hQtoPF%dlpviJ}u58T@DV6V1?{+aKJCxqJwUzN|p(%fAl+=cwg&iD| zGQaI&PIUMelJY`vj{V8v8(dQuwZF3;_cuF#lBJba$q~g$=@jtM>mbP6&Uu zLex1*GGP8I+rAaHeii>&e@z$v_Kw;7|E^zCq70YxGVVMa{qeG%Rmi?=zs27j-5Bxx zYU;ZVE0E0T?Yg%r=H3hboOb6skACo$67RYH`?HcA3U%CO-njSv@x8-)#lM5RE*Wr; zfsy$}_`)N65As`FSU#FF8#~NU=Sd_&(D|e!N=gNC95g)sFzsFCKh^oXvnxo_)78&q Iol`;+0Ci@BApigX literal 448 zcmeAS@N?(olHy`uVBq!ia0vp^T0ktt!3-q7rQ00=QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`a{_!qTyLq$-cpu*7$|;IN%q4Ep1bDKzh5xl)|R=gC38z%_C=-8|NjiP zRAe6oiGSI{`SUjGj~lE%uCxAm$8^t1>it5Ve?J%>g^K@rz;eex`qzDyXE`Dtmhybr z&-wiv`{PKlTWYe;ONDOh$UH9*0$RxZx8ov^QYi`Y3;s_SFhuC^@&eU!7I;J!GcfQS z24TkI`72U@f@?ip978Pp&rZC_*KEMUVi?Z!tgS$P>CgZ5I$Ok=)I9bm82a~_MrF!6 z$33&Vy5m4_sDbZ-!pAOKb8asJQl&B17Zy5)N4A9Qlizu(vRDGbR!2_VaN1^NsQKz!SH7r}th&1Bg;nF*k8!uI)JZTN zTR8v4U+3jk>nm3>fBsz&m8bP0 Hl+XkKu%*pp From 83ef61e92877dfaa7b506df2982d983162c8f0cc Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Sat, 17 Aug 2024 19:10:27 -0400 Subject: [PATCH 042/142] Cw 565 sign messages (#1378) * version bump to 3.13.9, auth working on mac * bump flutter version in workflow file * workflow fix * test fix * downgrade flutter version * test fix * test fix * update gradle version * start working on ui for message signing * updates * sign working for a few wallet types * updates & verification for electrum currencies * nano support * sign/verify working on eth, bitcoin broken * update translations * Implement Verify Message for Monero * save [skip ci] * pub key extraction working * fixes for electrum signing * verify working for solana! * electrum still not working :( [skip ci] * electrum messages working! * fixes for updated dart version, localization file updates * remove accidental inclusion * missed some unimplemented throws * Update res/values/strings_de.arb Co-authored-by: Konstantin Ullrich * Apply suggestions from code review Co-authored-by: Konstantin Ullrich * review suggestions and updates [skip ci] * [skip ci] add polygon * [skip ci] merge mac-auth/update version * fix litecoin * bio auth mac fix * remove comment and change duration from 2 to 0 * cherry pick previous changes * litecoin fixes, sign form fixes, use new walletAddressPicker * support accounts * verify messages working for monero * working sign and verify messages for nano * electrum signing working [skip ci] * additional nano fixes * update translations * attempt to decode signatures with base64 * workaround for secure storage bug on mac * bump version to 3.19.5 (because breez will need this version anyways) * some code cleanup * some changess didn't get saved * just documenting the issue [skip ci] * undo accidental removal + minor code cleanup * merge conflicts * merge fixes [skip ci] * add tron support * [wip] fixing * remove duplicate references to electrum path for maintainability * fixes * minor fix * fixes * undo debug comment * update migration for all electrum based wallets * hotfixes * copy over the rest of the fixes * minor code cleanup [skip ci] * updates * electrum signing workinggit statusgit statusgit statusgit status! * copy same fixes for litecoin * litecoin fixes * add v to litecoin signatures * fix dependencies * fix bitcoin_base version * merge fix * dep override * fix conflicts with main * trial fix for android build * fixes * fix * dep fix, should build * fix signing for bitcoin cash * [skip ci] minor code cleanup * [skip ci] minor code cleanup 2 * forgot wonero, various other fixes * more fixes * fix solana (untested) --------- Co-authored-by: Konstantin Ullrich Co-authored-by: Omar Hatem --- cw_bitcoin/lib/electrum_wallet.dart | 73 ++++++- cw_bitcoin/lib/litecoin_wallet.dart | 131 +++++++++++- cw_bitcoin/pubspec.yaml | 8 +- .../lib/src/bitcoin_cash_wallet.dart | 11 +- cw_bitcoin_cash/pubspec.yaml | 8 +- cw_core/lib/wallet_base.dart | 5 +- cw_evm/lib/evm_chain_wallet.dart | 20 +- cw_evm/pubspec.yaml | 2 + cw_monero/lib/api/wallet.dart | 4 + cw_monero/lib/monero_wallet.dart | 8 + cw_nano/lib/nano_block_info_response.dart | 37 ++++ cw_nano/lib/nano_client.dart | 87 ++++++-- cw_nano/lib/nano_wallet.dart | 25 ++- cw_nano/pubspec.lock | 8 +- cw_nano/pubspec.yaml | 3 +- cw_solana/lib/solana_wallet.dart | 54 ++++- cw_tron/lib/tron_wallet.dart | 14 +- cw_wownero/lib/api/wallet.dart | 4 + cw_wownero/lib/wownero_wallet.dart | 7 + lib/bitcoin/cw_bitcoin.dart | 4 +- lib/buy/dfx/dfx_buy_provider.dart | 4 +- lib/buy/robinhood/robinhood_buy_provider.dart | 6 +- lib/di.dart | 16 +- lib/router.dart | 14 ++ lib/routes.dart | 2 + .../cake_pay_confirm_purchase_card_page.dart | 18 +- .../dashboard/pages/cake_features_page.dart | 14 ++ lib/src/screens/dashboard/sign_page.dart | 202 ++++++++++++++++++ .../screens/dashboard/widgets/sign_form.dart | 98 +++++++++ .../dashboard/widgets/verify_form.dart | 92 ++++++++ .../screens/receive/address_list_page.dart | 31 +++ lib/src/screens/receive/receive_page.dart | 104 +-------- .../screens/receive/widgets/address_list.dart | 120 +++++++++++ lib/src/widgets/address_text_field.dart | 194 ++++++++++------- .../dashboard/dashboard_view_model.dart | 36 +++- lib/view_model/dashboard/sign_view_model.dart | 55 +++++ .../wallet_address_list_view_model.dart | 9 + lib/view_model/wallet_creation_vm.dart | 8 +- pubspec_base.yaml | 6 +- res/values/strings_ar.arb | 8 + res/values/strings_bg.arb | 8 + res/values/strings_cs.arb | 8 + res/values/strings_de.arb | 8 + res/values/strings_en.arb | 8 + res/values/strings_es.arb | 8 + res/values/strings_fr.arb | 8 + res/values/strings_ha.arb | 8 + res/values/strings_hi.arb | 8 + res/values/strings_hr.arb | 8 + res/values/strings_id.arb | 8 + res/values/strings_it.arb | 8 + res/values/strings_ja.arb | 8 + res/values/strings_ko.arb | 8 + res/values/strings_my.arb | 8 + res/values/strings_nl.arb | 8 + res/values/strings_pl.arb | 8 + res/values/strings_pt.arb | 8 + res/values/strings_ru.arb | 8 + res/values/strings_th.arb | 8 + res/values/strings_tl.arb | 8 + res/values/strings_tr.arb | 8 + res/values/strings_uk.arb | 8 + res/values/strings_ur.arb | 8 + res/values/strings_yo.arb | 8 + res/values/strings_zh.arb | 8 + 65 files changed, 1479 insertions(+), 271 deletions(-) create mode 100644 cw_nano/lib/nano_block_info_response.dart create mode 100644 lib/src/screens/dashboard/sign_page.dart create mode 100644 lib/src/screens/dashboard/widgets/sign_form.dart create mode 100644 lib/src/screens/dashboard/widgets/verify_form.dart create mode 100644 lib/src/screens/receive/address_list_page.dart create mode 100644 lib/src/screens/receive/widgets/address_list.dart create mode 100644 lib/view_model/dashboard/sign_view_model.dart diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 9dc8de083..8f2360f26 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -42,6 +42,7 @@ import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; +import 'package:http/http.dart' as http; import 'package:sp_scanner/sp_scanner.dart'; part 'electrum_wallet.g.dart'; @@ -132,6 +133,7 @@ abstract class ElectrumWalletBase final String? _mnemonic; Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); + Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1)); final EncryptionFileUtils encryptionFileUtils; final String? passphrase; @@ -591,7 +593,7 @@ abstract class ElectrumWalletBase } final derivationPath = - "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? electrum_path)}" "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" "/${utx.bitcoinAddressRecord.index}"; publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); @@ -1869,11 +1871,70 @@ abstract class ElectrumWalletBase ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index : null; final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index)); - final priv = ECPrivate.fromWif( - WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer), - netVersion: network.wifNetVer, - ); - return priv.signMessage(StringUtils.encode(message)); + final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex()); + + String messagePrefix = '\x18Bitcoin Signed Message:\n'; + final hexEncoded = priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix); + final decodedSig = hex.decode(hexEncoded); + return base64Encode(decodedSig); + } + + @override + Future verifyMessage(String message, String signature, {String? address = null}) async { + if (address == null) { + return false; + } + + List sigDecodedBytes = []; + + if (signature.endsWith('=')) { + sigDecodedBytes = base64.decode(signature); + } else { + sigDecodedBytes = hex.decode(signature); + } + + if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) { + throw ArgumentException( + "signature must be 64 bytes without recover-id or 65 bytes with recover-id"); + } + + String messagePrefix = '\x18Bitcoin Signed Message:\n'; + final messageHash = QuickCrypto.sha256Hash( + BitcoinSignerUtils.magicMessage(utf8.encode(message), messagePrefix)); + + List correctSignature = + sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes); + List rBytes = correctSignature.sublist(0, 32); + List sBytes = correctSignature.sublist(32); + final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes)); + + List possibleRecoverIds = [0, 1]; + + final baseAddress = addressTypeFromStr(address, network); + + for (int recoveryId in possibleRecoverIds) { + final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId); + + final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes()); + + String? recoveredAddress; + + if (baseAddress is P2pkAddress) { + recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network); + } else if (baseAddress is P2pkhAddress) { + recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network); + } else if (baseAddress is P2wshAddress) { + recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network); + } else if (baseAddress is P2wpkhAddress) { + recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network); + } + + if (recoveredAddress == address) { + return true; + } + } + + return false; } Future _setInitialHeight() async { diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index d8c04dba6..890d98342 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,6 +1,9 @@ -import 'package:bip39/bip39.dart' as bip39; +import 'dart:convert'; + import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:blockchain_utils/signer/ecdsa_signing_key.dart'; +import 'package:bip39/bip39.dart' as bip39; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -17,6 +20,9 @@ import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; +import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; +import 'package:pointycastle/ecc/api.dart'; +import 'package:pointycastle/ecc/curves/secp256k1.dart'; part 'litecoin_wallet.g.dart'; @@ -167,4 +173,127 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return 0; } + + @override + Future signMessage(String message, {String? address = null}) async { + final index = address != null + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index + : null; + final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index)); + final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex()); + + final privateKey = ECDSAPrivateKey.fromBytes( + priv.toBytes(), + Curves.generatorSecp256k1, + ); + + final signature = + signLitecoinMessage(utf8.encode(message), privateKey: privateKey, bipPrive: priv.prive); + + return base64Encode(signature); + } + + List _magicPrefix(List message, List messagePrefix) { + final encodeLength = IntUtils.encodeVarint(message.length); + + return [...messagePrefix, ...encodeLength, ...message]; + } + + List signLitecoinMessage(List message, + {required ECDSAPrivateKey privateKey, required Bip32PrivateKey bipPrive}) { + String messagePrefix = '\x19Litecoin Signed Message:\n'; + final messageHash = QuickCrypto.sha256Hash(magicMessage(message, messagePrefix)); + final signingKey = EcdsaSigningKey(privateKey); + ECDSASignature ecdsaSign = + signingKey.signDigestDeterminstic(digest: messageHash, hashFunc: () => SHA256()); + final n = Curves.generatorSecp256k1.order! >> 1; + BigInt newS; + if (ecdsaSign.s.compareTo(n) > 0) { + newS = Curves.generatorSecp256k1.order! - ecdsaSign.s; + } else { + newS = ecdsaSign.s; + } + final rawSig = ECDSASignature(ecdsaSign.r, newS); + final rawSigBytes = rawSig.toBytes(BitcoinSignerUtils.baselen); + + final pub = bipPrive.publicKey; + final ECDomainParameters curve = ECCurve_secp256k1(); + final point = curve.curve.decodePoint(pub.point.toBytes()); + + final rawSigEc = ECSignature(rawSig.r, rawSig.s); + + final recId = SignUtils.findRecoveryId( + SignUtils.getHexString(messageHash, offset: 0, length: messageHash.length), + rawSigEc, + Uint8List.fromList(pub.uncompressed), + ); + + final v = recId + 27 + (point!.isCompressed ? 4 : 0); + + final combined = Uint8List.fromList([v, ...rawSigBytes]); + + return combined; + } + + List magicMessage(List message, String messagePrefix) { + final prefixBytes = StringUtils.encode(messagePrefix); + final magic = _magicPrefix(message, prefixBytes); + return QuickCrypto.sha256Hash(magic); + } + + @override + Future verifyMessage(String message, String signature, {String? address = null}) async { + if (address == null) { + return false; + } + + List sigDecodedBytes = []; + + if (signature.endsWith('=')) { + sigDecodedBytes = base64.decode(signature); + } else { + sigDecodedBytes = hex.decode(signature); + } + + if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) { + throw ArgumentException( + "litecoin signature must be 64 bytes without recover-id or 65 bytes with recover-id"); + } + + String messagePrefix = '\x19Litecoin Signed Message:\n'; + final messageHash = QuickCrypto.sha256Hash(magicMessage(utf8.encode(message), messagePrefix)); + + List correctSignature = + sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes); + List rBytes = correctSignature.sublist(0, 32); + List sBytes = correctSignature.sublist(32); + final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes)); + + List possibleRecoverIds = [0, 1]; + + final baseAddress = addressTypeFromStr(address, network); + + for (int recoveryId in possibleRecoverIds) { + final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId); + final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes()); + + String? recoveredAddress; + + if (baseAddress is P2pkAddress) { + recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network); + } else if (baseAddress is P2pkhAddress) { + recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network); + } else if (baseAddress is P2wshAddress) { + recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network); + } else if (baseAddress is P2wpkhAddress) { + recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network); + } + + if (recoveredAddress == address) { + return true; + } + } + + return false; + } } diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 449833220..2af1ac54e 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -25,10 +25,6 @@ dependencies: ref: Add-Support-For-OP-Return-data rxdart: ^0.27.5 cryptography: ^2.0.5 - bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v4 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils @@ -57,6 +53,10 @@ dependency_overrides: url: https://github.com/cake-tech/ledger-flutter.git ref: cake-v3 watcher: ^1.1.0 + bitcoin_base: + git: + url: https://github.com/cake-tech/bitcoin_base + ref: cake-update-v5 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index a59569ae6..5659528c0 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -202,11 +202,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { @override Future signMessage(String message, {String? address = null}) async { - final index = address != null - ? walletAddresses.allAddresses - .firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address)) - .index - : null; + int? index; + try { + index = address != null + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index + : null; + } catch (_) {} final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index)); final priv = ECPrivate.fromWif( WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer), diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 3728bafc5..64bd38b1d 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -25,10 +25,6 @@ dependencies: git: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data - bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v4 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils @@ -43,6 +39,10 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 + bitcoin_base: + git: + url: https://github.com/cake-tech/bitcoin_base + ref: cake-update-v5 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index f7af15224..14ba898a7 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -69,7 +69,6 @@ abstract class WalletBase renameWalletFiles(String newWalletName); - Future signMessage(String message, {String? address = null}) => throw UnimplementedError(); + Future signMessage(String message, {String? address = null}); + + Future verifyMessage(String message, String signature, {String? address = null}); bool? isTestnet; } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 80a366e6f..bbf972f0d 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -35,6 +35,7 @@ import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; +import 'package:eth_sig_util/eth_sig_util.dart'; import 'evm_chain_transaction_info.dart'; import 'evm_erc20_balance.dart'; @@ -500,7 +501,7 @@ abstract class EVMChainWalletBase } final methodSignature = - transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null; + transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null; return methodSignatureToType[methodSignature]; } @@ -692,8 +693,21 @@ abstract class EVMChainWalletBase } @override - Future signMessage(String message, {String? address}) async => - bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message))); + Future signMessage(String message, {String? address}) async { + return bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message))); + } + + @override + Future verifyMessage(String message, String signature, {String? address}) async { + if (address == null) { + return false; + } + final recoveredAddress = EthSigUtil.recoverPersonalSignature( + message: ascii.encode(message), + signature: signature, + ); + return recoveredAddress.toUpperCase() == address.toUpperCase(); + } Web3Client? getWeb3Client() => _client.getWeb3Client(); diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index b24e375a7..3e12834b1 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -13,6 +13,8 @@ dependencies: flutter: sdk: flutter web3dart: ^2.7.1 + eth_sig_util: ^0.0.9 + erc20: ^1.0.1 bip39: ^1.0.6 bip32: ^2.0.0 hex: ^0.2.0 diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 1a6e5315d..b493e536e 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -316,3 +316,7 @@ Future trustedDaemon() async => monero.Wallet_trustedDaemon(wptr!); String signMessage(String message, {String address = ""}) { return monero.Wallet_signMessage(wptr!, message: message, address: address); } + +bool verifyMessage(String message, String address, String signature) { + return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature); +} \ No newline at end of file diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 31e09ca2d..f5fa0ec7e 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -783,4 +783,12 @@ abstract class MoneroWalletBase extends WalletBase verifyMessage(String message, String signature, {String? address = null}) async { + if (address == null) return false; + + return monero_wallet.verifyMessage(message, address, signature); + } + } diff --git a/cw_nano/lib/nano_block_info_response.dart b/cw_nano/lib/nano_block_info_response.dart new file mode 100644 index 000000000..d2f000b9d --- /dev/null +++ b/cw_nano/lib/nano_block_info_response.dart @@ -0,0 +1,37 @@ +class BlockContentsResponse { + String type; + String account; + String previous; + String representative; + String balance; + String link; + String linkAsAccount; + String signature; + String work; + + BlockContentsResponse({ + required this.type, + required this.account, + required this.previous, + required this.representative, + required this.balance, + required this.link, + required this.linkAsAccount, + required this.signature, + required this.work, + }); + + factory BlockContentsResponse.fromJson(Map json) { + return BlockContentsResponse( + type: json['type'] as String, + account: json['account'] as String, + previous: json['previous'] as String, + representative: json['representative'] as String, + balance: json['balance'] as String, + link: json['link'] as String, + linkAsAccount: json['link_as_account'] as String, + signature: json['signature'] as String, + work: json['work'] as String, + ); + } +} diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index 8d8bef13d..478a6c125 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'dart:convert'; import 'package:cw_core/nano_account_info_response.dart'; +import 'package:cw_nano/nano_block_info_response.dart'; import 'package:cw_core/n2_node.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_transaction_model.dart'; import 'package:http/http.dart' as http; -import 'package:nanodart/nanodart.dart'; import 'package:cw_core/node.dart'; import 'package:nanoutil/nanoutil.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -111,6 +111,27 @@ class NanoClient { } } + Future getBlockContents(String block) async { + try { + final response = await http.post( + _node!.uri, + headers: CAKE_HEADERS, + body: jsonEncode( + { + "action": "block_info", + "json_block": "true", + "hash": block, + }, + ), + ); + final data = await jsonDecode(response.body); + return BlockContentsResponse.fromJson(data["contents"] as Map); + } catch (e) { + print("error while getting block info $e"); + return null; + } + } + Future changeRep({ required String privateKey, required String repAddress, @@ -135,8 +156,8 @@ class NanoClient { }; // sign the change block: - final String hash = NanoBlocks.computeStateHash( - NanoAccountType.NANO, + final String hash = NanoSignatures.computeStateHash( + NanoBasedCurrency.NANO, changeBlock["account"]!, changeBlock["previous"]!, changeBlock["representative"]!, @@ -248,7 +269,7 @@ class NanoClient { } final String representative = infoResponse.representative; // link = destination address: - final String link = NanoAccounts.extractPublicKey(destinationAddress); + final String link = NanoDerivations.addressToPublicKey(destinationAddress); final String linkAsAccount = destinationAddress; // construct the send block: @@ -262,8 +283,8 @@ class NanoClient { }; // sign the send block: - final String hash = NanoBlocks.computeStateHash( - NanoAccountType.NANO, + final String hash = NanoSignatures.computeStateHash( + NanoBasedCurrency.NANO, sendBlock["account"]!, sendBlock["previous"]!, sendBlock["representative"]!, @@ -285,7 +306,6 @@ class NanoClient { Future receiveBlock({ required String blockHash, - required String source, required String amountRaw, required String destinationAddress, required String privateKey, @@ -310,15 +330,56 @@ class NanoClient { representative = infoData.representative; } + if ((BigInt.tryParse(amountRaw) ?? BigInt.zero) <= BigInt.zero) { + throw Exception("amountRaw must be greater than zero"); + } + + BlockContentsResponse? frontierContents; + + if (!openBlock) { + // get the block info of the frontier block: + frontierContents = await getBlockContents(frontier); + + if (frontierContents == null) { + throw Exception("error while getting frontier block info"); + } + + final String frontierHash = NanoSignatures.computeStateHash( + NanoBasedCurrency.NANO, + frontierContents.account, + frontierContents.previous, + frontierContents.representative, + BigInt.parse(frontierContents.balance), + frontierContents.link, + ); + + bool valid = await NanoSignatures.verify( + frontierHash, + frontierContents.signature, + destinationAddress, + ); + + if (!valid) { + throw Exception( + "Frontier block signature is invalid! Potentially malicious block detected!"); + } + } + // first get the account balance: - final BigInt currentBalance = (await getBalance(destinationAddress)).currentBalance; + late BigInt currentBalance; + if (!openBlock) { + currentBalance = BigInt.parse(frontierContents!.balance); + } else { + currentBalance = BigInt.zero; + } final BigInt txAmount = BigInt.parse(amountRaw); final BigInt balanceAfterTx = currentBalance + txAmount; // link = send block hash: final String link = blockHash; // this "linkAsAccount" is meaningless: - final String linkAsAccount = NanoAccounts.createAccount(NanoAccountType.NANO, blockHash); + final String linkAsAccount = + NanoDerivations.publicKeyToAddress(blockHash, currency: NanoBasedCurrency.NANO); // construct the receive block: Map receiveBlock = { @@ -332,8 +393,8 @@ class NanoClient { }; // sign the receive block: - final String hash = NanoBlocks.computeStateHash( - NanoAccountType.NANO, + final String hash = NanoSignatures.computeStateHash( + NanoBasedCurrency.NANO, receiveBlock["account"]!, receiveBlock["previous"]!, receiveBlock["representative"]!, @@ -345,7 +406,7 @@ class NanoClient { // get PoW for the receive block: String? work; if (openBlock) { - work = await requestWork(NanoAccounts.extractPublicKey(destinationAddress)); + work = await requestWork(NanoDerivations.addressToPublicKey(destinationAddress)); } else { work = await requestWork(frontier); } @@ -409,10 +470,8 @@ class NanoClient { for (final blockHash in blocks.keys) { final block = blocks[blockHash]; final String amountRaw = block["amount"] as String; - final String source = block["source"] as String; await receiveBlock( blockHash: blockHash, - source: source, amountRaw: amountRaw, privateKey: privateKey, destinationAddress: destinationAddress, diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index cba8d09a0..700710c2e 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -27,7 +27,6 @@ import 'package:cw_nano/nano_wallet_addresses.dart'; import 'package:cw_nano/nano_wallet_keys.dart'; import 'package:cw_nano/pending_nano_transaction.dart'; import 'package:mobx/mobx.dart'; -import 'package:nanodart/nanodart.dart'; import 'package:nanoutil/nanoutil.dart'; part 'nano_wallet.g.dart'; @@ -107,7 +106,6 @@ abstract class NanoWalletBase if (_derivationType == DerivationType.unknown) { _derivationType = DerivationType.nano; } - final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd"; // our "mnemonic" is actually a hex form seed: if (!_mnemonic.contains(' ')) { @@ -122,8 +120,10 @@ abstract class NanoWalletBase _hexSeed = await NanoDerivations.hdMnemonicListToSeed(_mnemonic.split(' ')); } } - NanoDerivationType derivationType = - type == "standard" ? NanoDerivationType.STANDARD : NanoDerivationType.HD; + + final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd"; + NanoDerivationType derivationType = NanoDerivations.stringToType(type); + _privateKey = await NanoDerivations.universalSeedToPrivate( _hexSeed!, index: 0, @@ -216,8 +216,8 @@ abstract class NanoWalletBase balanceAfterTx: runningBalance, previousHash: previousHash, ); - previousHash = NanoBlocks.computeStateHash( - NanoAccountType.NANO, + previousHash = NanoSignatures.computeStateHash( + NanoBasedCurrency.NANO, block["account"]!, block["previous"]!, block["representative"]!, @@ -535,4 +535,17 @@ abstract class NanoWalletBase // Delete old name's dir and files await Directory(currentDirPath).delete(recursive: true); } + + @override + Future signMessage(String message, {String? address = null}) async { + return NanoSignatures.signMessage(message, privateKey!); + } + + @override + Future verifyMessage(String message, String signature, {String? address = null}) async { + if (address == null) { + return false; + } + return await NanoSignatures.verifyMessage(message, signature, address); + } } diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index bbe909199..ef9de14f9 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -513,7 +513,7 @@ packages: source: hosted version: "2.3.0" nanodart: - dependency: "direct main" + dependency: transitive description: name: nanodart sha256: "4b2f42d60307b54e8cf384d6193a567d07f8efd773858c0d5948246153c13282" @@ -524,11 +524,11 @@ packages: dependency: "direct main" description: path: "." - ref: c37e72817cf0a28162f43124f79661d6c8e0098f - resolved-ref: c37e72817cf0a28162f43124f79661d6c8e0098f + ref: c01a9c552917008d8fbc6b540db657031625b04f + resolved-ref: c01a9c552917008d8fbc6b540db657031625b04f url: "https://github.com/perishllc/nanoutil.git" source: git - version: "1.0.0" + version: "1.0.3" package_config: dependency: transitive description: diff --git a/cw_nano/pubspec.yaml b/cw_nano/pubspec.yaml index 6fae6a895..3ddd9769e 100644 --- a/cw_nano/pubspec.yaml +++ b/cw_nano/pubspec.yaml @@ -15,7 +15,6 @@ dependencies: mobx: ^2.0.7+4 bip39: ^1.0.6 bip32: ^2.0.0 - nanodart: ^2.0.0 decimal: ^2.3.3 libcrypto: ^0.2.2 ed25519_hd_key: ^2.2.0 @@ -25,7 +24,7 @@ dependencies: nanoutil: git: url: https://github.com/perishllc/nanoutil.git - ref: c37e72817cf0a28162f43124f79661d6c8e0098f + ref: c01a9c552917008d8fbc6b540db657031625b04f cw_core: path: ../cw_core diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 66b8bca42..4e69db3b8 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -32,6 +32,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:solana/base58.dart'; import 'package:solana/metaplex.dart' as metaplex; import 'package:solana/solana.dart'; +import 'package:solana/src/crypto/ed25519_hd_keypair.dart'; +import 'package:cryptography/cryptography.dart'; part 'solana_wallet.g.dart'; @@ -571,17 +573,59 @@ abstract class SolanaWalletBase }); } - Future signSolanaMessage(String message) async { + @override + Future signMessage(String message, {String? address}) async { // Convert the message to bytes final messageBytes = utf8.encode(message); // Sign the message bytes with the wallet's private key - final signature = await _walletKeyPair!.sign(messageBytes); + final signature = (await _walletKeyPair!.sign(messageBytes)).toString(); - // Convert the signature to a hexadecimal string - final hex = HEX.encode(signature.bytes); + return HEX.encode(utf8.encode(signature)).toUpperCase(); + } - return hex; + List> bytesFromSigString(String signatureString) { + final regex = RegExp(r'Signature\(\[(.+)\], publicKey: (.+)\)'); + final match = regex.firstMatch(signatureString); + + if (match != null) { + final bytesString = match.group(1)!; + final base58EncodedPublicKeyString = match.group(2)!; + final sigBytes = bytesString.split(', ').map(int.parse).toList(); + + List pubKeyBytes = base58decode(base58EncodedPublicKeyString); + + return [sigBytes, pubKeyBytes]; + } else { + throw const FormatException('Invalid Signature string format'); + } + } + + @override + Future verifyMessage(String message, String signature, {String? address}) async { + String signatureString = utf8.decode(HEX.decode(signature)); + + List> bytes = bytesFromSigString(signatureString); + + final messageBytes = utf8.encode(message); + final sigBytes = bytes[0]; + final pubKeyBytes = bytes[1]; + + if (address == null) { + return false; + } + + // make sure the address derived from the public key provided matches the one we expect + final pub = Ed25519HDPublicKey(pubKeyBytes); + if (address != pub.toBase58()) { + return false; + } + + return await verifySignature( + message: messageBytes, + signature: sigBytes, + publicKey: Ed25519HDPublicKey(pubKeyBytes), + ); } SolanaClient? get solanaClient => _client.getSolanaClient; diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index 7dc43b4bb..f5841d894 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -580,8 +580,18 @@ abstract class TronWalletBase } @override - Future signMessage(String message, {String? address}) async => - _tronPrivateKey.signPersonalMessage(ascii.encode(message)); + Future signMessage(String message, {String? address}) async { + return _tronPrivateKey.signPersonalMessage(ascii.encode(message)); + } + + @override + Future verifyMessage(String message, String signature, {String? address}) async { + if (address == null) { + return false; + } + TronPublicKey pubKey = TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!; + return pubKey.toAddress().toString() == address; + } String getTronBase58AddressFromHex(String hexAddress) => TronAddress(hexAddress).toAddress(); diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 0a06a9950..56f54dfac 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -320,3 +320,7 @@ Future trustedDaemon() async => wownero.Wallet_trustedDaemon(wptr!); String signMessage(String message, {String address = ""}) { return wownero.Wallet_signMessage(wptr!, message: message, address: address); } + +bool verifyMessage(String message, String address, String signature) { + return wownero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature); +} \ No newline at end of file diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 85f5e4b2f..c3f4bcb69 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -743,4 +743,11 @@ abstract class WowneroWalletBase final useAddress = address ?? ""; return wownero_wallet.signMessage(message, address: useAddress); } + + @override + Future verifyMessage(String message, String signature, {String? address = null}) async { + if (address == null) return false; + + return wownero_wallet.verifyMessage(message, address, signature); + } } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 989cd2b35..e87773f97 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -275,7 +275,7 @@ class CWBitcoin extends Bitcoin { return [DerivationType.bip39, DerivationType.electrum]; } - int _countOccurrences(String str, String charToCount) { + int _countCharOccurrences(String str, String charToCount) { int count = 0; for (int i = 0; i < str.length; i++) { if (str[i] == charToCount) { @@ -330,7 +330,7 @@ class CWBitcoin extends Bitcoin { ); String balancePath = dInfoCopy.derivationPath!; - int derivationDepth = _countOccurrences(balancePath, "/"); + int derivationDepth = _countCharOccurrences(balancePath, '/'); // for BIP44 if (derivationDepth == 3 || derivationDepth == 1) { diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index 2a7e2ab13..b3ed72498 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -124,12 +124,12 @@ class DFXBuyProvider extends BuyProvider { switch (wallet.type) { case WalletType.ethereum: case WalletType.polygon: - return wallet.signMessage(message); + return await wallet.signMessage(message); case WalletType.monero: case WalletType.litecoin: case WalletType.bitcoin: case WalletType.bitcoinCash: - return wallet.signMessage(message, address: walletAddress); + return await wallet.signMessage(message, address: walletAddress); default: throw Exception("WalletType is not available for DFX ${wallet.type}"); } diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index ab58754dd..2d809772e 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -37,15 +37,15 @@ class RobinhoodBuyProvider extends BuyProvider { String get _apiSecret => secrets.exchangeHelperApiKey; - Future getSignature(String message) { + Future getSignature(String message) async { switch (wallet.type) { case WalletType.ethereum: case WalletType.polygon: - return wallet.signMessage(message); + return await wallet.signMessage(message); case WalletType.litecoin: case WalletType.bitcoin: case WalletType.bitcoinCash: - return wallet.signMessage(message, address: wallet.walletAddresses.address); + return await wallet.signMessage(message, address: wallet.walletAddresses.address); default: throw Exception("WalletType is not available for Robinhood ${wallet.type}"); } diff --git a/lib/di.dart b/lib/di.dart index 7c22e809c..1967c9227 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -30,6 +30,12 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; +import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; +import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/template.dart'; @@ -159,7 +165,6 @@ import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/node.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; -import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; @@ -179,7 +184,6 @@ import 'package:cake_wallet/store/templates/exchange_template_store.dart'; import 'package:cake_wallet/store/templates/send_template_store.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; -import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/view_model/backup_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart'; @@ -193,7 +197,6 @@ import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; -import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; @@ -224,7 +227,6 @@ import 'package:cake_wallet/view_model/wallet_unlock_loadable_view_model.dart'; import 'package:cake_wallet/view_model/wallet_unlock_verifiable_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; @@ -853,6 +855,8 @@ Future setup({ getIt.registerFactoryParam( (ContactRecord? contact, _) => ContactPage(getIt.get(param1: contact))); + getIt.registerFactory(() => AddressListPage(getIt.get())); + getIt.registerFactory(() { final appStore = getIt.get(); return NodeListViewModel(_nodeSource, appStore); @@ -1271,9 +1275,11 @@ Future setup({ getIt.registerFactory( () => WalletConnectConnectionsView(web3walletService: getIt.get())); - + getIt.registerFactory(() => NFTViewModel(appStore, getIt.get())); getIt.registerFactory(() => TorPage(getIt.get())); + getIt.registerFactory(() => SignViewModel(getIt.get().wallet!)); + _isSetupFinished = true; } diff --git a/lib/router.dart b/lib/router.dart index 498077511..25af39043 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -27,6 +27,7 @@ import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/sign_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; @@ -42,6 +43,8 @@ import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart'; +import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; +import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; @@ -99,6 +102,7 @@ import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; @@ -465,6 +469,9 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(param1: selectedCurrency)); + case Routes.pickerWalletAddress: + return MaterialPageRoute(builder: (_) => getIt.get()); + case Routes.addressBookAddContact: return CupertinoPageRoute( builder: (_) => getIt.get(param1: settings.arguments as ContactRecord?)); @@ -696,6 +703,13 @@ Route createRoute(RouteSettings settings) { case Routes.torPage: return MaterialPageRoute(builder: (_) => getIt.get()); + case Routes.signPage: + return MaterialPageRoute( + builder: (_) => SignPage( + getIt.get(), + ), + ); + case Routes.connectDevices: final params = settings.arguments as ConnectDevicePageParams; return MaterialPageRoute( diff --git a/lib/routes.dart b/lib/routes.dart index caa7eb39e..9c421cab5 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -31,6 +31,7 @@ class Routes { static const nanoAccountCreation = '/nano_account_new'; static const addressBook = '/address_book'; static const pickerAddressBook = '/picker_address_book'; + static const pickerWalletAddress = '/picker_wallet_address'; static const addressBookAddContact = '/address_book_add_contact'; static const showKeys = '/show_keys'; static const exchangeConfirm = '/exchange_confirm'; @@ -103,5 +104,6 @@ class Routes { static const nftDetailsPage = '/nft_details_page'; static const importNFTPage = '/import_nft_page'; static const torPage = '/tor_page'; + static const signPage = '/sign_page'; static const connectDevices = '/device/connect'; } diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart index 02ddf037d..81f6a354f 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -322,31 +322,31 @@ class CakePayBuyCardDetailPage extends BasePage { await showPopUp( context: context, - builder: (_) { + builder: (popupContext) { return Observer( builder: (_) => ConfirmSendingAlert( - alertTitle: S.of(context).confirm_sending, - paymentId: S.of(context).payment_id, + alertTitle: S.of(popupContext).confirm_sending, + paymentId: S.of(popupContext).payment_id, paymentIdValue: order?.orderId, expirationTime: cakePayPurchaseViewModel.formattedRemainingTime, onDispose: () => _handleDispose(disposer), - amount: S.of(context).send_amount, + amount: S.of(popupContext).send_amount, amountValue: pendingTransaction.amountFormatted, fiatAmountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, - fee: S.of(context).send_fee, + fee: S.of(popupContext).send_fee, feeValue: pendingTransaction.feeFormatted, feeFiatAmount: cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, feeRate: pendingTransaction.feeRate, outputs: cakePayPurchaseViewModel.sendViewModel.outputs, - rightButtonText: S.of(context).send, - leftButtonText: S.of(context).cancel, + rightButtonText: S.of(popupContext).send, + leftButtonText: S.of(popupContext).cancel, actionRightButton: () async { - Navigator.of(context).pop(); + Navigator.of(popupContext).pop(); await cakePayPurchaseViewModel.sendViewModel.commitTransaction(); }, - actionLeftButton: () => Navigator.of(context).pop())); + actionLeftButton: () => Navigator.of(popupContext).pop())); }, ); } diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index b034fb799..0c953c892 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -64,6 +65,19 @@ class CakeFeaturesPage extends StatelessWidget { subTitle: S.of(context).nanogpt_subtitle, onTap: () => _launchUrl("cake.nano-gpt.com"), ), + SizedBox(height: 20), + Observer( + builder: (context) { + if (!dashboardViewModel.hasSignMessages) { + return const SizedBox(); + } + return DashBoardRoundedCardWidget( + onTap: () => Navigator.of(context).pushNamed(Routes.signPage), + title: S.current.sign_verify_message, + subTitle: S.current.sign_verify_message_sub, + ); + }, + ), ], ), ), diff --git a/lib/src/screens/dashboard/sign_page.dart b/lib/src/screens/dashboard/sign_page.dart new file mode 100644 index 000000000..05cf63728 --- /dev/null +++ b/lib/src/screens/dashboard/sign_page.dart @@ -0,0 +1,202 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/sign_form.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/verify_form.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; +import 'package:cake_wallet/themes/extensions/wallet_list_theme.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/dashboard/sign_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:mobx/mobx.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; + +class SignPage extends BasePage { + SignPage(this.signViewModel) + : signFormKey = GlobalKey(), + verifyFormKey = GlobalKey(), + _pages = [], + _controller = PageController(initialPage: 0) { + _pages.add(SignForm( + key: signFormKey, + type: signViewModel.wallet.type, + includeAddress: signViewModel.signIncludesAddress, + )); + _pages.add(VerifyForm( + key: verifyFormKey, + type: signViewModel.wallet.type, + )); + } + + @override + Widget middle(BuildContext context) => Observer( + builder: (_) => Text( + S.current.sign_verify_title, + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: titleColor(context), + ), + )); + + final SignViewModel signViewModel; + final PageController _controller; + final List _pages; + final GlobalKey signFormKey; + final GlobalKey verifyFormKey; + bool _isEffectsInstalled = false; + + @override + Widget body(BuildContext context) { + _setEffects(context); + + return KeyboardActions( + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: FocusNode(), + toolbarButtons: [(_) => KeyboardDoneButton()], + ) + ], + ), + child: Container( + height: 0, + color: Theme.of(context).colorScheme.background, + child: Center( + child: ConstrainedBox( + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: PageView.builder( + onPageChanged: (page) { + signViewModel.isSigning = page == 0; + }, + controller: _controller, + itemCount: _pages.length, + itemBuilder: (_, index) => SingleChildScrollView(child: _pages[index]), + ), + ), + if (_pages.length > 1) + Padding( + padding: EdgeInsets.only(top: 10), + child: SmoothPageIndicator( + controller: _controller, + count: _pages.length, + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).hintColor.withOpacity(0.5), + activeDotColor: Theme.of(context).hintColor, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24), + child: Column( + children: [ + Observer( + builder: (context) { + return LoadingPrimaryButton( + onPressed: () async { + await _confirmForm(context); + }, + text: signViewModel.isSigning + ? S.current.sign_message + : S.current.verify_message, + color: Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor, + textColor: Theme.of(context) + .extension()! + .restoreWalletButtonTextColor, + isLoading: signViewModel.state is IsExecutingState, + isDisabled: signViewModel.state is IsExecutingState, + ); + }, + ), + ], + ), + ) + ], + ), + ), + ), + ), + ); + } + + void _setEffects(BuildContext context) async { + if (_isEffectsInstalled) { + return; + } + _isEffectsInstalled = true; + + reaction((_) => signViewModel.state, (ExecutionState state) { + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (_) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }); + }); + } + if (state is ExecutedSuccessfullyState) { + if (signViewModel.isSigning) { + signFormKey.currentState!.signatureController.text = state.payload as String; + } else { + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (_) { + return AlertWithOneAction( + alertTitle: S.current.successful, + alertContent: S.current.message_verified, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }); + }); + } + } + }); + } + + Future _confirmForm(BuildContext context) async { + FocusManager.instance.primaryFocus?.unfocus(); + + if (signViewModel.isSigning) { + String message = signFormKey.currentState!.messageController.text; + String? address; + if (signViewModel.signIncludesAddress) { + address = signFormKey.currentState!.addressController.text; + } + await signViewModel.sign(message, address: address); + } else { + String message = verifyFormKey.currentState!.messageController.text; + String signature = verifyFormKey.currentState!.signatureController.text; + String address = verifyFormKey.currentState!.addressController.text; + await signViewModel.verify(message, signature, address: address); + } + } +} diff --git a/lib/src/screens/dashboard/widgets/sign_form.dart b/lib/src/screens/dashboard/widgets/sign_form.dart new file mode 100644 index 000000000..c0f8ba328 --- /dev/null +++ b/lib/src/screens/dashboard/widgets/sign_form.dart @@ -0,0 +1,98 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/address_text_field.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class SignForm extends StatefulWidget { + SignForm({ + Key? key, + required this.type, + required this.includeAddress, + }) : super(key: key); + + final WalletType type; + final bool includeAddress; + + @override + SignFormState createState() => SignFormState(); +} + +class SignFormState extends State { + SignFormState() + : formKey = GlobalKey(), + messageController = TextEditingController(), + addressController = TextEditingController(), + signatureController = TextEditingController(); + + final TextEditingController messageController; + final TextEditingController addressController; + final TextEditingController signatureController; + final GlobalKey formKey; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(left: 24, right: 24), + child: Column( + children: [ + Form( + key: formKey, + child: Column( + children: [ + AddressTextField( + controller: messageController, + placeholder: S.current.message, + options: [AddressTextFieldOption.paste], + buttonColor: Theme.of(context).hintColor, + ), + if (widget.includeAddress) ...[ + const SizedBox(height: 20), + AddressTextField( + controller: addressController, + options: [ + AddressTextFieldOption.paste, + AddressTextFieldOption.walletAddresses + ], + buttonColor: Theme.of(context).hintColor, + onSelectedContact: (contact) { + addressController.text = contact.address; + }, + selectedCurrency: walletTypeToCryptoCurrency(widget.type), + ), + ], + ], + )), + const SizedBox(height: 20), + GestureDetector( + onTap: () async { + final text = signatureController.text; + if (text.isEmpty) { + return; + } + Clipboard.setData(ClipboardData(text: text)); + showBar(context, S.of(context).transaction_details_copied(text)); + }, + child: BaseTextFormField( + enabled: false, + controller: signatureController, + hintText: S.current.signature, + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/dashboard/widgets/verify_form.dart b/lib/src/screens/dashboard/widgets/verify_form.dart new file mode 100644 index 000000000..d59261494 --- /dev/null +++ b/lib/src/screens/dashboard/widgets/verify_form.dart @@ -0,0 +1,92 @@ +import 'package:cake_wallet/core/wallet_name_validator.dart'; +import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/address_text_field.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; +import 'package:cake_wallet/src/widgets/seed_widget.dart'; +import 'package:cake_wallet/themes/extensions/address_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mobx/mobx.dart'; +import 'package:polyseed/polyseed.dart'; + +class VerifyForm extends StatefulWidget { + VerifyForm({ + Key? key, + required this.type, + }) : super(key: key); + + final WalletType type; + + @override + VerifyFormState createState() => VerifyFormState(); +} + +class VerifyFormState extends State { + VerifyFormState() + : formKey = GlobalKey(), + messageController = TextEditingController(), + addressController = TextEditingController(), + signatureController = TextEditingController(); + + final TextEditingController messageController; + final TextEditingController addressController; + final TextEditingController signatureController; + final GlobalKey formKey; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(left: 24, right: 24), + child: Form( + key: formKey, + child: Column( + children: [ + AddressTextField( + controller: messageController, + placeholder: S.current.message, + options: [AddressTextFieldOption.paste], + buttonColor: Theme.of(context).hintColor, + ), + const SizedBox(height: 20), + AddressTextField( + controller: addressController, + options: [AddressTextFieldOption.paste, AddressTextFieldOption.walletAddresses], + buttonColor: Theme.of(context).hintColor, + onSelectedContact: (contact) { + addressController.text = contact.address; + }, + selectedCurrency: walletTypeToCryptoCurrency(widget.type), + ), + const SizedBox(height: 20), + AddressTextField( + controller: signatureController, + placeholder: S.current.signature, + options: [AddressTextFieldOption.paste], + buttonColor: Theme.of(context).hintColor, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/receive/address_list_page.dart b/lib/src/screens/receive/address_list_page.dart new file mode 100644 index 000000000..5f6794715 --- /dev/null +++ b/lib/src/screens/receive/address_list_page.dart @@ -0,0 +1,31 @@ +import 'package:cake_wallet/src/screens/receive/widgets/address_list.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; + +class AddressListPage extends BasePage { + AddressListPage(this.addressListViewModel); + + final WalletAddressListViewModel addressListViewModel; + + @override + String get title => S.current.accounts_subaddresses; + + @override + Widget body(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + AddressList( + addressListViewModel: addressListViewModel, + onSelect: (String address) async { + Navigator.of(context).pop(address); + }, + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 03524ef79..724e5c3bd 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/address_list.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; @@ -122,108 +123,7 @@ class ReceivePage extends BasePage { amountController: _amountController, isLight: currentTheme.type == ThemeType.light), ), - Observer( - builder: (_) => ListView.separated( - padding: EdgeInsets.all(0), - separatorBuilder: (context, _) => const HorizontalSectionDivider(), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: addressListViewModel.items.length, - itemBuilder: (context, index) { - final item = addressListViewModel.items[index]; - Widget cell = Container(); - - if (item is WalletAccountListHeader) { - cell = HeaderTile( - showTrailingButton: true, - walletAddressListViewModel: addressListViewModel, - trailingButtonTap: () async { - if (addressListViewModel.type == WalletType.monero || - addressListViewModel.type == WalletType.wownero || - addressListViewModel.type == WalletType.haven) { - await showPopUp( - context: context, - builder: (_) => getIt.get()); - } else { - await showPopUp( - context: context, - builder: (_) => getIt.get()); - } - }, - title: S.of(context).accounts, - trailingIcon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).extension()!.iconsColor, - )); - } - - if (item is WalletAddressListHeader) { - final hasTitle = item.title != null; - - cell = HeaderTile( - title: hasTitle ? item.title! : S.of(context).addresses, - walletAddressListViewModel: addressListViewModel, - showTrailingButton: - !addressListViewModel.isAutoGenerateSubaddressEnabled && !hasTitle, - showSearchButton: true, - trailingButtonTap: () => - Navigator.of(context).pushNamed(Routes.newSubaddress), - trailingIcon: hasTitle - ? null - : Icon( - Icons.add, - size: 20, - color: - Theme.of(context).extension()!.iconsColor, - ), - ); - } - - if (item is WalletAddressListItem) { - cell = Observer(builder: (_) { - final isCurrent = item.address == addressListViewModel.address.address; - final backgroundColor = isCurrent - ? Theme.of(context) - .extension()! - .currentTileBackgroundColor - : Theme.of(context) - .extension()! - .tilesBackgroundColor; - final textColor = isCurrent - ? Theme.of(context) - .extension()! - .currentTileTextColor - : Theme.of(context).extension()!.tilesTextColor; - - return AddressCell.fromItem( - item, - isCurrent: isCurrent, - hasBalance: addressListViewModel.isElectrumWallet, - backgroundColor: backgroundColor, - textColor: textColor, - onTap: item.isOneTimeReceiveAddress == true - ? null - : (_) => addressListViewModel.setAddress(item), - onEdit: item.isOneTimeReceiveAddress == true || item.isPrimary - ? null - : () => Navigator.of(context) - .pushNamed(Routes.newSubaddress, arguments: item), - onDelete: !addressListViewModel.isSilentPayments || item.isPrimary - ? null - : () => addressListViewModel.deleteAddress(item), - ); - }); - } - - return index != 0 - ? cell - : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), topRight: Radius.circular(30)), - child: cell, - ); - })), + AddressList(addressListViewModel: addressListViewModel), Padding( padding: EdgeInsets.fromLTRB(24, 24, 24, 32), child: Text( diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart new file mode 100644 index 000000000..8dfbedec1 --- /dev/null +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -0,0 +1,120 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; +import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart'; +import 'package:cake_wallet/src/widgets/section_divider.dart'; +import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class AddressList extends StatelessWidget { + const AddressList({ + super.key, + required this.addressListViewModel, + this.onSelect, + }); + + final WalletAddressListViewModel addressListViewModel; + final Function(String)? onSelect; + + @override + Widget build(BuildContext context) { + bool editable = onSelect == null; + return Observer( + builder: (_) => ListView.separated( + padding: EdgeInsets.all(0), + separatorBuilder: (context, _) => const HorizontalSectionDivider(), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: addressListViewModel.items.length, + itemBuilder: (context, index) { + final item = addressListViewModel.items[index]; + Widget cell = Container(); + + if (item is WalletAccountListHeader) { + cell = HeaderTile( + showTrailingButton: true, + walletAddressListViewModel: addressListViewModel, + trailingButtonTap: () async { + if (addressListViewModel.type == WalletType.monero || + addressListViewModel.type == WalletType.haven) { + await showPopUp( + context: context, builder: (_) => getIt.get()); + } else { + await showPopUp( + context: context, builder: (_) => getIt.get()); + } + }, + title: S.of(context).accounts, + trailingIcon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + )); + } + + if (item is WalletAddressListHeader) { + cell = HeaderTile( + title: S.of(context).addresses, + walletAddressListViewModel: addressListViewModel, + showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled, + showSearchButton: true, + trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress), + trailingIcon: Icon( + Icons.add, + size: 20, + color: Theme.of(context).extension()!.iconsColor, + )); + } + + if (item is WalletAddressListItem) { + cell = Observer(builder: (_) { + final isCurrent = item.address == addressListViewModel.address.address && editable; + final backgroundColor = isCurrent + ? Theme.of(context).extension()!.currentTileBackgroundColor + : Theme.of(context).extension()!.tilesBackgroundColor; + final textColor = isCurrent + ? Theme.of(context).extension()!.currentTileTextColor + : Theme.of(context).extension()!.tilesTextColor; + + return AddressCell.fromItem( + item, + isCurrent: isCurrent, + hasBalance: addressListViewModel.isElectrumWallet, + backgroundColor: backgroundColor, + textColor: textColor, + onTap: (_) { + if (onSelect != null) { + onSelect!(item.address); + return; + } + addressListViewModel.setAddress(item); + }, + onEdit: editable + ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item) + : null, + ); + }); + } + + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), topRight: Radius.circular(30)), + child: cell, + ); + }, + ), + ); + } +} diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index f79da8069..f229ea8ef 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -12,7 +12,7 @@ import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart'; -enum AddressTextFieldOption { paste, qrCode, addressBook } +enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses } class AddressTextField extends StatelessWidget { AddressTextField( @@ -34,6 +34,7 @@ class AddressTextField extends StatelessWidget { this.validator, this.onPushPasteButton, this.onPushAddressBookButton, + this.onPushAddressPickerButton, this.onSelectedContact, this.selectedCurrency}); @@ -56,6 +57,7 @@ class AddressTextField extends StatelessWidget { final FocusNode? focusNode; final Function(BuildContext context)? onPushPasteButton; final Function(BuildContext context)? onPushAddressBookButton; + final Function(BuildContext context)? onPushAddressPickerButton; final Function(ContactBase contact)? onSelectedContact; final CryptoCurrency? selectedCurrency; @@ -70,16 +72,13 @@ class AddressTextField extends StatelessWidget { enabled: isActive, controller: controller, focusNode: focusNode, - style: textStyle ?? TextStyle( fontSize: 16, color: Theme.of(context).extension()!.titleColor), decoration: InputDecoration( - suffixIcon: SizedBox( width: prefixIconWidth * options.length + (spaceBetweenPrefixIcons * options.length), ), - hintStyle: hintStyle ?? TextStyle(fontSize: 16, color: Theme.of(context).hintColor), hintText: placeholder ?? S.current.widgets_address, focusedBorder: isBorderExist @@ -104,90 +103,122 @@ class AddressTextField extends StatelessWidget { top: 2, right: 0, child: SizedBox( - width: prefixIconWidth * options.length + (spaceBetweenPrefixIcons * options.length), + width: + (prefixIconWidth * options.length) + (spaceBetweenPrefixIcons * options.length), child: Row( mainAxisAlignment: responsiveLayoutUtil.shouldRenderMobileUI ? MainAxisAlignment.spaceBetween : MainAxisAlignment.end, children: [ - SizedBox(width: 5), if (this.options.contains(AddressTextFieldOption.paste)) ...[ + SizedBox(width: 5), Container( - width: prefixIconWidth, - height: prefixIconHeight, - padding: EdgeInsets.only(top: 0), - child: Semantics( - label: S.of(context).paste, - child: InkWell( - onTap: () async => _pasteAddress(context), - child: Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: buttonColor ?? - Theme.of(context).dialogTheme.backgroundColor, - borderRadius: BorderRadius.all(Radius.circular(6))), - child: Image.asset( - 'assets/images/paste_ios.png', - color: iconColor ?? - Theme.of(context) - .extension()! - .textFieldButtonIconColor, - )), - ), - )), + width: prefixIconWidth, + height: prefixIconHeight, + padding: EdgeInsets.only(top: 0), + child: Semantics( + label: S.of(context).paste, + child: InkWell( + onTap: () async => _pasteAddress(context), + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: + buttonColor ?? Theme.of(context).dialogTheme.backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(6))), + child: Image.asset( + 'assets/images/paste_ios.png', + color: iconColor ?? + Theme.of(context) + .extension()! + .textFieldButtonIconColor, + )), + ), + ), + ), ], if (this.options.contains(AddressTextFieldOption.qrCode) && DeviceInfo.instance.isMobile) ...[ - Container( - width: prefixIconWidth, - height: prefixIconHeight, - padding: EdgeInsets.only(top: 0), - child: Semantics( - label: S.of(context).scan_qr_code, - child: InkWell( - onTap: () async => _presentQRScanner(context), - child: Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: buttonColor ?? - Theme.of(context).dialogTheme.backgroundColor, - borderRadius: BorderRadius.all(Radius.circular(6))), - child: Image.asset( - 'assets/images/qr_code_icon.png', - color: iconColor ?? - Theme.of(context) - .extension()! - .textFieldButtonIconColor, - )), - ), - )) - ] else SizedBox(width: 5), - if (this.options.contains(AddressTextFieldOption.addressBook)) ...[ Container( - width: prefixIconWidth, - height: prefixIconHeight, - padding: EdgeInsets.only(top: 0), - child: Semantics( - label: S.of(context).address_book, - child: InkWell( - onTap: () async => _presetAddressBookPicker(context), - child: Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: buttonColor ?? - Theme.of(context).dialogTheme.backgroundColor, - borderRadius: BorderRadius.all(Radius.circular(6))), - child: Image.asset( - 'assets/images/open_book.png', - color: iconColor ?? - Theme.of(context) - .extension()! - .textFieldButtonIconColor, - )), - ), - )) - ] + width: prefixIconWidth, + height: prefixIconHeight, + padding: EdgeInsets.only(top: 0), + child: Semantics( + label: S.of(context).scan_qr_code, + child: InkWell( + onTap: () async => _presentQRScanner(context), + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: + buttonColor ?? Theme.of(context).dialogTheme.backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(6))), + child: Image.asset( + 'assets/images/qr_code_icon.png', + color: iconColor ?? + Theme.of(context) + .extension()! + .textFieldButtonIconColor, + )), + ), + ), + ), + ], + if (this.options.contains(AddressTextFieldOption.addressBook)) ...[ + SizedBox(width: 5), + Container( + width: prefixIconWidth, + height: prefixIconHeight, + padding: EdgeInsets.only(top: 0), + child: Semantics( + label: S.of(context).address_book, + child: InkWell( + onTap: () async => _presetAddressBookPicker(context), + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: + buttonColor ?? Theme.of(context).dialogTheme.backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(6))), + child: Image.asset( + 'assets/images/open_book.png', + color: iconColor ?? + Theme.of(context) + .extension()! + .textFieldButtonIconColor, + )), + ), + ), + ), + ], + if (this.options.contains(AddressTextFieldOption.walletAddresses)) ...[ + SizedBox(width: 5), + Container( + width: prefixIconWidth, + height: prefixIconHeight, + padding: EdgeInsets.only(top: 0), + child: Semantics( + label: S.of(context).address_book, + child: InkWell( + onTap: () async => _presetWalletAddressPicker(context), + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: + buttonColor ?? Theme.of(context).dialogTheme.backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(6))), + child: Image.asset( + 'assets/images/open_book.png', + color: iconColor ?? + Theme.of(context) + .extension()! + .textFieldButtonIconColor, + )), + ), + ), + ) + ], ], ), )) @@ -197,7 +228,7 @@ class AddressTextField extends StatelessWidget { Future _presentQRScanner(BuildContext context) async { bool isCameraPermissionGranted = - await PermissionHandler.checkPermission(Permission.camera, context); + await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; final code = await presentQRScanner(); if (code.isEmpty) { @@ -224,6 +255,15 @@ class AddressTextField extends StatelessWidget { } } + Future _presetWalletAddressPicker(BuildContext context) async { + final address = await Navigator.of(context).pushNamed(Routes.pickerWalletAddress); + + if (address is String) { + controller?.text = address; + onPushAddressPickerButton?.call(context); + } + } + Future _pasteAddress(BuildContext context) async { final clipboard = await Clipboard.getData('text/plain'); final address = clipboard?.text ?? ''; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index e98412dce..56a0e061b 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -182,7 +182,8 @@ abstract class DashboardViewModelBase with Store { final _accountTransactions = _wallet.transactionHistory.transactions.values .where((tx) => - wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id) + wow.wownero!.getTransactionInfoAccountId(tx) == + wow.wownero!.getCurrentAccount(wallet).id) .toList(); final sortedTransactions = [..._accountTransactions]; @@ -482,6 +483,27 @@ abstract class DashboardViewModelBase with Store { @computed bool get hasPowNodes => wallet.type == WalletType.nano || wallet.type == WalletType.banano; + @computed + bool get hasSignMessages { + switch (wallet.type) { + case WalletType.monero: + case WalletType.litecoin: + case WalletType.bitcoin: + case WalletType.bitcoinCash: + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + case WalletType.nano: + case WalletType.banano: + case WalletType.tron: + case WalletType.wownero: + return true; + case WalletType.haven: + case WalletType.none: + return false; + } + } + bool get showRepWarning { if (wallet.type != WalletType.nano) { return false; @@ -575,7 +597,8 @@ abstract class DashboardViewModelBase with Store { } if (wallet.type == WalletType.wownero) { - return wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id; + return wow.wownero!.getTransactionInfoAccountId(tx) == + wow.wownero!.getCurrentAccount(wallet).id; } return true; @@ -600,8 +623,8 @@ abstract class DashboardViewModelBase with Store { .getTransactionHistory(wallet) .transactions .values - .where( - (tx) => monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) + .where((tx) => + monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) .toList(); transactions.addAll(_accountTransactions.map((transaction) => TransactionListItem( @@ -613,8 +636,9 @@ abstract class DashboardViewModelBase with Store { .getTransactionHistory(wallet) .transactions .values - .where( - (tx) => wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id) + .where((tx) => + wow.wownero!.getTransactionInfoAccountId(tx) == + wow.wownero!.getCurrentAccount(wallet).id) .toList(); transactions.addAll(_accountTransactions.map((transaction) => TransactionListItem( diff --git a/lib/view_model/dashboard/sign_view_model.dart b/lib/view_model/dashboard/sign_view_model.dart new file mode 100644 index 000000000..5b1b4fc00 --- /dev/null +++ b/lib/view_model/dashboard/sign_view_model.dart @@ -0,0 +1,55 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:mobx/mobx.dart'; + +part 'sign_view_model.g.dart'; + +class SignViewModel = SignViewModelBase with _$SignViewModel; + +abstract class SignViewModelBase with Store { + SignViewModelBase(this.wallet) : state = InitialExecutionState(); + + final WalletBase wallet; + + @observable + ExecutionState state; + + @observable + bool isSigning = true; + + bool get signIncludesAddress => [ + WalletType.monero, + WalletType.bitcoin, + WalletType.bitcoinCash, + WalletType.litecoin, + WalletType.haven, + ].contains(wallet.type); + + @action + Future sign(String message, {String? address}) async { + state = IsExecutingState(); + try { + final signature = await wallet.signMessage(message, address: address); + state = ExecutedSuccessfullyState(payload: signature); + } catch (e) { + state = FailureState(e.toString()); + } + } + + @action + Future verify(String message, String signature, {String? address}) async { + state = IsExecutingState(); + try { + final sig = await wallet.verifyMessage(message, signature, address: address); + if (sig) { + state = ExecutedSuccessfullyState(); + } else { + state = FailureState(S.current.signature_invalid_error); + } + } catch (e) { + state = FailureState(e.toString()); + } + } +} diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 6c274bb7b..0bd936720 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -438,6 +439,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.nano) { + addressList.add(WalletAddressListItem( + isPrimary: true, + name: null, + address: wallet.walletAddresses.address, + )); + } + if (wallet.type == WalletType.tron) { final primaryAddress = tron!.getAddress(wallet); diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 5a9a1d093..e14934986 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -117,9 +117,7 @@ abstract class WalletCreationVMBase with Store { DerivationInfo? getDefaultDerivation() { switch (this.type) { case WalletType.nano: - return DerivationInfo( - derivationType: DerivationType.nano, - ); + return DerivationInfo(derivationType: DerivationType.nano); case WalletType.bitcoin: case WalletType.litecoin: return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; @@ -131,9 +129,7 @@ abstract class WalletCreationVMBase with Store { DerivationInfo? getCommonRestoreDerivation() { switch (this.type) { case WalletType.nano: - return DerivationInfo( - derivationType: DerivationType.nano, - ); + return DerivationInfo(derivationType: DerivationType.nano); case WalletType.bitcoin: return DerivationInfo( derivationType: DerivationType.bip39, diff --git a/pubspec_base.yaml b/pubspec_base.yaml index a2f346bae..07dc7f5af 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -103,7 +103,7 @@ dependencies: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v4 + ref: cake-update-v5 ledger_flutter: ^1.0.1 hashlib: 1.12.0 @@ -138,6 +138,10 @@ dependency_overrides: url: https://github.com/cake-tech/web3dart.git ref: cake flutter_secure_storage_platform_interface: 1.0.2 + bitcoin_base: + git: + url: https://github.com/cake-tech/bitcoin_base + ref: cake-update-v5 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index a73067383..f84f6102b 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -370,6 +370,7 @@ "max_value": "الحد الأقصى: ${value} ${currency}", "memo": "مذكرة:", "message": "ﺔﻟﺎﺳﺭ", + "message_verified": "تم التحقق من الرسالة بنجاح", "methods": " ﻕﺮﻃُ", "min_amount": "الحد الأدنى: ${value}", "min_value": "الحد الأدنى: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "اظهار السييد / المفاتيح", "show_market_place": "إظهار السوق", "show_seed": "عرض السييد", + "sign_message": "تسجيل رسالة", "sign_up": "اشتراك", + "sign_verify_message": "توقيع أو التحقق من الرسالة", + "sign_verify_message_sub": "قم بتوقيع أو التحقق من رسالة باستخدام المفتاح الخاص بك", + "sign_verify_title": "تسجيل / تحقق", + "signature": "إمضاء", + "signature_invalid_error": "التوقيع غير صالح للرسالة المقدمة", "signTransaction": " ﺔﻠﻣﺎﻌﻤﻟﺍ ﻊﻴﻗﻮﺗ", "signup_for_card_accept_terms": "قم بالتسجيل للحصول على البطاقة وقبول الشروط.", "silent_payments": "مدفوعات صامتة", @@ -822,6 +829,7 @@ "value_type": "نوع القيمة", "variable_pair_not_supported": "هذا الزوج المتغير غير مدعوم في التبادلات المحددة", "verification": "تَحَقّق", + "verify_message": "تحقق من الرسالة", "verify_with_2fa": "تحقق مع Cake 2FA", "version": "الإصدار ${currentVersion}", "view_all": "مشاهدة الكل", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index b60cbc55c..0b6b688bb 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -370,6 +370,7 @@ "max_value": "Макс: ${value} ${currency}", "memo": "Мемо:", "message": "Съобщение", + "message_verified": "Съобщението беше успешно проверено", "methods": "Методи", "min_amount": "Мин: ${value}", "min_value": "Мин: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "Покажи seed/keys", "show_market_place": "Покажи пазар", "show_seed": "Покажи seed", + "sign_message": "Съобщение за подписване", "sign_up": "Регистрация", + "sign_verify_message": "Подпишете или проверете съобщението", + "sign_verify_message_sub": "Подпишете или проверете съобщение с помощта на вашия личен ключ", + "sign_verify_title": "Подпишете / проверете", + "signature": "Подпис", + "signature_invalid_error": "Подписът не е валиден за даденото съобщение", "signTransaction": "Подпишете транзакция", "signup_for_card_accept_terms": "Регистрайте се за картата и приемете условията.", "silent_payments": "Мълчаливи плащания", @@ -822,6 +829,7 @@ "value_type": "Тип стойност", "variable_pair_not_supported": "Този variable pair не се поддържа от избраната борса", "verification": "Потвърждаване", + "verify_message": "Проверете съобщението", "verify_with_2fa": "Проверете с Cake 2FA", "version": "Версия ${currentVersion}", "view_all": "Виж всички", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 537695dd9..ef225041d 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Memo:", "message": "Zpráva", + "message_verified": "Zpráva byla úspěšně ověřena", "methods": "Metody", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "Zobrazit seed/klíče", "show_market_place": "Zobrazit trh", "show_seed": "Zobrazit seed", + "sign_message": "Podepsat zprávu", "sign_up": "Registrovat se", + "sign_verify_message": "Podepište nebo ověřte zprávu", + "sign_verify_message_sub": "Podepište nebo ověřte zprávu pomocí soukromého klíče", + "sign_verify_title": "Podepsat / ověřit", + "signature": "Podpis", + "signature_invalid_error": "Podpis není platný pro danou zprávu", "signTransaction": "Podepsat transakci", "signup_for_card_accept_terms": "Zaregistrujte se pro kartu a souhlaste s podmínkami.", "silent_payments": "Tiché platby", @@ -822,6 +829,7 @@ "value_type": "Typ hodnoty", "variable_pair_not_supported": "Tento pár s tržním kurzem není ve zvolené směnárně podporován", "verification": "Ověření", + "verify_message": "Ověřit zprávu", "verify_with_2fa": "Ověřte pomocí Cake 2FA", "version": "Verze ${currentVersion}", "view_all": "Zobrazit vše", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 052015367..393c3d928 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Memo:", "message": "Nachricht", + "message_verified": "Die Nachricht wurde erfolgreich überprüft", "methods": "Methoden", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", @@ -662,7 +663,13 @@ "show_keys": "Seed/Schlüssel anzeigen", "show_market_place": "Marktplatz anzeigen", "show_seed": "Seed zeigen", + "sign_message": "Nachricht unterschreiben", "sign_up": "Anmelden", + "sign_verify_message": "Nachricht unterschreiben oder überprüfen", + "sign_verify_message_sub": "Unterschreiben oder überprüfen Sie eine Nachricht mit Ihrem privaten Schlüssel", + "sign_verify_title": "Zeichen / überprüfen", + "signature": "Signatur", + "signature_invalid_error": "Die Signatur gilt nicht für die angegebene Nachricht", "signTransaction": "Transaktion unterzeichnen", "signup_for_card_accept_terms": "Melden Sie sich für die Karte an und akzeptieren Sie die Bedingungen.", "silent_payments": "Stille Zahlungen", @@ -824,6 +831,7 @@ "value_type": "Werttyp", "variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt", "verification": "Verifizierung", + "verify_message": "Nachricht überprüfen", "verify_with_2fa": "Verifizieren Sie mit Cake 2FA", "version": "Version ${currentVersion}", "view_all": "Alle anzeigen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 0f0ebd470..4ac7dd8fd 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Memo:", "message": "Message", + "message_verified": "The message was successfully verified", "methods": "Methods", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", @@ -662,7 +663,13 @@ "show_keys": "Show seed/keys", "show_market_place": "Show Marketplace", "show_seed": "Show seed", + "sign_message": "Sign Message", "sign_up": "Sign Up", + "sign_verify_message": "Sign or verify message", + "sign_verify_message_sub": "Sign or verify a message using your private key", + "sign_verify_title": "Sign / Verify", + "signature": "Signature", + "signature_invalid_error": "The signature is not valid for the message given", "signTransaction": "Sign Transaction", "signup_for_card_accept_terms": "Sign up for the card and accept the terms.", "silent_payments": "Silent Payments", @@ -823,6 +830,7 @@ "value_type": "Value Type", "variable_pair_not_supported": "This variable pair is not supported with the selected exchanges", "verification": "Verification", + "verify_message": "Verify Message", "verify_with_2fa": "Verify with Cake 2FA", "version": "Version ${currentVersion}", "view_all": "View all", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 1e6eeae59..02d17ac96 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Memorándum:", "message": "Mensaje", + "message_verified": "El mensaje fue verificado con éxito", "methods": "Métodos", "min_amount": "Mínimo: ${value}", "min_value": "Min: ${value} ${currency}", @@ -662,7 +663,13 @@ "show_keys": "Mostrar semilla/claves", "show_market_place": "Mostrar mercado", "show_seed": "Mostrar semilla", + "sign_message": "Mensaje de firma", "sign_up": "Registrarse", + "sign_verify_message": "Firmar o verificar el mensaje", + "sign_verify_message_sub": "Firmar o verificar un mensaje usando su clave privada", + "sign_verify_title": "Firmar / verificar", + "signature": "Firma", + "signature_invalid_error": "La firma no es válida para el mensaje dado", "signTransaction": "Firmar transacción", "signup_for_card_accept_terms": "Regístrese para obtener la tarjeta y acepte los términos.", "silent_payments": "Pagos silenciosos", @@ -823,6 +830,7 @@ "value_type": "Tipo de valor", "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados", "verification": "Verificación", + "verify_message": "Mensaje de verificación", "verify_with_2fa": "Verificar con Cake 2FA", "version": "Versión ${currentVersion}", "view_all": "Ver todo", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index a64a69514..4bc5c9809 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Mémo :", "message": "Message", + "message_verified": "Le message a été vérifié avec succès", "methods": "Méthodes", "min_amount": "Min : ${value}", "min_value": "Min: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "Visualiser la phrase secrète (seed) et les clefs", "show_market_place": "Afficher la place de marché", "show_seed": "Visualiser la phrase secrète (seed)", + "sign_message": "Signer le message", "sign_up": "S'inscrire", + "sign_verify_message": "Signer ou vérifier le message", + "sign_verify_message_sub": "Signez ou vérifiez un message en utilisant votre clé privée", + "sign_verify_title": "Signe / vérifier", + "signature": "Signature", + "signature_invalid_error": "La signature n'est pas valable pour le message donné", "signTransaction": "Signer une transaction", "signup_for_card_accept_terms": "Inscrivez-vous pour la carte et acceptez les conditions.", "silent_payments": "Paiements silencieux", @@ -822,6 +829,7 @@ "value_type": "Type de valeur", "variable_pair_not_supported": "Cette paire variable n'est pas prise en charge avec les échanges sélectionnés", "verification": "Vérification", + "verify_message": "Vérifier le message", "verify_with_2fa": "Vérifier avec Cake 2FA", "version": "Version ${currentVersion}", "view_all": "Voir tout", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 219b3c939..025a33f6b 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Memo:", "message": "Sako", + "message_verified": "An yi nasarar tabbatar da sakon", "methods": "Hanyoyin", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", @@ -663,7 +664,13 @@ "show_keys": "Nuna iri/maɓallai", "show_market_place": "Nuna dan kasuwa", "show_seed": "Nuna iri", + "sign_message": "Sa hannu", "sign_up": "Shiga", + "sign_verify_message": "Shiga ko Tabbatar da Saƙo", + "sign_verify_message_sub": "Shiga ko tabbatar da saƙo ta amfani da Maɓallinku na sirri", + "sign_verify_title": "Sa hannu / Tabbatar", + "signature": "Sa hannu", + "signature_invalid_error": "Sa hannu ba shi da inganci ga sakon da aka bayar", "signTransaction": "Sa hannu Ma'amala", "signup_for_card_accept_terms": "Yi rajista don katin kuma karɓi sharuɗɗan.", "silent_payments": "Biya silent", @@ -824,6 +831,7 @@ "value_type": "Nau'in darajar", "variable_pair_not_supported": "Ba a samun goyan bayan wannan m biyu tare da zaɓaɓɓun musayar", "verification": "tabbatar", + "verify_message": "Tabbatar saƙon", "verify_with_2fa": "Tabbatar da Cake 2FA", "version": "Sigar ${currentVersion}", "view_all": "Duba duka", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index b711939a4..346c420a9 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -370,6 +370,7 @@ "max_value": "मैक्स: ${value} ${currency}", "memo": "ज्ञापन:", "message": "संदेश", + "message_verified": "संदेश को सफलतापूर्वक सत्यापित किया गया था", "methods": "तरीकों", "min_amount": "न्यूनतम: ${value}", "min_value": "मिन: ${value} ${currency}", @@ -663,7 +664,13 @@ "show_keys": "बीज / कुंजियाँ दिखाएँ", "show_market_place": "बाज़ार दिखाएँ", "show_seed": "बीज दिखाओ", + "sign_message": "हस्ताक्षर संदेश", "sign_up": "साइन अप करें", + "sign_verify_message": "संदेश पर हस्ताक्षर या सत्यापित करें", + "sign_verify_message_sub": "अपनी निजी कुंजी का उपयोग करके किसी संदेश पर हस्ताक्षर या सत्यापित करें", + "sign_verify_title": "हस्ताक्षर / सत्यापित करें", + "signature": "हस्ताक्षर", + "signature_invalid_error": "हस्ताक्षर दिए गए संदेश के लिए मान्य नहीं है", "signTransaction": "लेन-देन पर हस्ताक्षर करें", "signup_for_card_accept_terms": "कार्ड के लिए साइन अप करें और शर्तें स्वीकार करें।", "silent_payments": "मूक भुगतान", @@ -824,6 +831,7 @@ "value_type": "मान प्रकार", "variable_pair_not_supported": "यह परिवर्तनीय जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", "verification": "सत्यापन", + "verify_message": "संदेश सत्यापित करें", "verify_with_2fa": "केक 2FA के साथ सत्यापित करें", "version": "संस्करण ${currentVersion}", "view_all": "सभी देखें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 401514735..6f5bc88a6 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -370,6 +370,7 @@ "max_value": "Maks.: ${value} ${currency}", "memo": "Memo:", "message": "Poruka", + "message_verified": "Poruka je uspješno provjerena", "methods": "Metode", "min_amount": "Minimalno: ${value}", "min_value": "Min.: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "Prikaži pristupni izraz/ključ", "show_market_place": "Prikaži tržište", "show_seed": "Prikaži pristupni izraz", + "sign_message": "Poruka", "sign_up": "Prijavite se", + "sign_verify_message": "Potpisati ili provjeriti poruku", + "sign_verify_message_sub": "Potpišite ili provjerite poruku pomoću privatnog ključa", + "sign_verify_title": "Potpisati / provjeriti", + "signature": "Potpis", + "signature_invalid_error": "Potpis ne vrijedi za danu poruku", "signTransaction": "Potpišite transakciju", "signup_for_card_accept_terms": "Prijavite se za karticu i prihvatite uvjete.", "silent_payments": "Tiha plaćanja", @@ -822,6 +829,7 @@ "value_type": "Tipa vrijednosti", "variable_pair_not_supported": "Ovaj par varijabli nije podržan s odabranim burzama", "verification": "Potvrda", + "verify_message": "Provjerite poruku", "verify_with_2fa": "Provjerite s Cake 2FA", "version": "Verzija ${currentVersion}", "view_all": "Prikaži sve", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index b6210b5dd..2bc298aa7 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Memo:", "message": "Pesan", + "message_verified": "Pesan itu berhasil diverifikasi", "methods": "Metode", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", @@ -664,7 +665,13 @@ "show_keys": "Tampilkan seed/kunci", "show_market_place": "Tampilkan Pasar", "show_seed": "Tampilkan seed", + "sign_message": "Pesan tanda", "sign_up": "Daftar", + "sign_verify_message": "Tanda tangan atau verifikasi pesan", + "sign_verify_message_sub": "Menandatangani atau memverifikasi pesan menggunakan kunci pribadi Anda", + "sign_verify_title": "Tanda / verifikasi", + "signature": "Tanda tangan", + "signature_invalid_error": "Tanda tangan tidak valid untuk pesan yang diberikan", "signTransaction": "Tandatangani Transaksi", "signup_for_card_accept_terms": "Daftar untuk kartu dan terima syarat dan ketentuan.", "silent_payments": "Pembayaran diam", @@ -825,6 +832,7 @@ "value_type": "Jenis Nilai", "variable_pair_not_supported": "Pasangan variabel ini tidak didukung dengan bursa yang dipilih", "verification": "Verifikasi", + "verify_message": "Verifikasi pesan", "verify_with_2fa": "Verifikasi dengan Cake 2FA", "version": "Versi ${currentVersion}", "view_all": "Lihat Semua", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index fc4abf7c4..0548b0ad2 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -371,6 +371,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Memo:", "message": "Messaggio", + "message_verified": "Il messaggio è stato verificato con successo", "methods": "Metodi", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", @@ -663,7 +664,13 @@ "show_keys": "Mostra seme/chiavi", "show_market_place": "Mostra mercato", "show_seed": "Mostra seme", + "sign_message": "Messaggio di firma", "sign_up": "Registrati", + "sign_verify_message": "Firmare o verificare il messaggio", + "sign_verify_message_sub": "Firma o verifica un messaggio utilizzando la chiave privata", + "sign_verify_title": "Firmare / verificare", + "signature": "Firma", + "signature_invalid_error": "La firma non è valida per il messaggio dato", "signTransaction": "Firma la transazione", "signup_for_card_accept_terms": "Registrati per la carta e accetta i termini.", "silent_payments": "Pagamenti silenziosi", @@ -824,6 +831,7 @@ "value_type": "Tipo di valore", "variable_pair_not_supported": "Questa coppia di variabili non è supportata con gli scambi selezionati", "verification": "Verifica", + "verify_message": "Verificare il messaggio", "verify_with_2fa": "Verifica con Cake 2FA", "version": "Versione ${currentVersion}", "view_all": "Visualizza tutto", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 98495fc8b..c53a6d001 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -371,6 +371,7 @@ "max_value": "マックス: ${value} ${currency}", "memo": "メモ:", "message": "メッセージ", + "message_verified": "メッセージは正常に検証されました", "methods": "メソッド", "min_amount": "最小: ${value}", "min_value": "分: ${value} ${currency}", @@ -662,7 +663,13 @@ "show_keys": "シード/キーを表示する", "show_market_place": "マーケットプレイスを表示", "show_seed": "シードを表示", + "sign_message": "署名メッセージ", "sign_up": "サインアップ", + "sign_verify_message": "メッセージに署名または確認します", + "sign_verify_message_sub": "秘密鍵を使用してメッセージに署名または確認します", + "sign_verify_title": "署名 /検証", + "signature": "サイン", + "signature_invalid_error": "署名は、指定されたメッセージに対して無効です", "signTransaction": "トランザクションに署名する", "signup_for_card_accept_terms": "カードにサインアップして、利用規約に同意してください。", "silent_payments": "サイレント支払い", @@ -823,6 +830,7 @@ "value_type": "値タイプ", "variable_pair_not_supported": "この変数ペアは、選択した取引所ではサポートされていません", "verification": "検証", + "verify_message": "メッセージを確認します", "verify_with_2fa": "Cake 2FA で検証する", "version": "バージョン ${currentVersion}", "view_all": "すべて表示", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 60c52b21f..be6757d99 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -370,6 +370,7 @@ "max_value": "맥스: ${value} ${currency}", "memo": "메모:", "message": "메시지", + "message_verified": "메시지가 성공적으로 확인되었습니다", "methods": "행동 양식", "min_amount": "최소: ${value}", "min_value": "최소: ${value} ${currency}", @@ -662,7 +663,13 @@ "show_keys": "시드 / 키 표시", "show_market_place": "마켓플레이스 표시", "show_seed": "종자 표시", + "sign_message": "서명 메시지", "sign_up": "가입", + "sign_verify_message": "메시지에 서명하거나 확인하십시오", + "sign_verify_message_sub": "개인 키를 사용하여 메시지에 서명하거나 확인하십시오", + "sign_verify_title": "서명 / 확인", + "signature": "서명", + "signature_invalid_error": "서명은 주어진 메시지에 유효하지 않습니다", "signTransaction": "거래 서명", "signup_for_card_accept_terms": "카드에 가입하고 약관에 동의합니다.", "silent_payments": "조용한 지불", @@ -823,6 +830,7 @@ "value_type": "가치 유형", "variable_pair_not_supported": "이 변수 쌍은 선택한 교환에서 지원되지 않습니다.", "verification": "검증", + "verify_message": "메시지를 확인하십시오", "verify_with_2fa": "케이크 2FA로 확인", "version": "버전 ${currentVersion}", "view_all": "모두 보기", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 42643be48..c53dffb6b 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -370,6 +370,7 @@ "max_value": "အများဆုံး- ${value} ${currency}", "memo": "မှတ်စုတို:", "message": "မက်ဆေ့ချ်", + "message_verified": "မက်ဆေ့ခ်ျကိုအောင်မြင်စွာအတည်ပြုခဲ့သည်", "methods": "နည်းလမ်းများ", "min_amount": "အနည်းဆုံး- ${value}", "min_value": "အနည်းဆုံး- ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "မျိုးစေ့ /သော့များကို ပြပါ။", "show_market_place": "စျေးကွက်ကိုပြသပါ။", "show_seed": "မျိုးစေ့ကိုပြပါ။", + "sign_message": "လက်မှတ်စာ", "sign_up": "ဆိုင်းအပ်", + "sign_verify_message": "မက်ဆေ့ခ်ျကိုလက်မှတ်ထိုးသို့မဟုတ်အတည်ပြုရန်", + "sign_verify_message_sub": "သင်၏ကိုယ်ပိုင်သော့ကို သုံး. မက်ဆေ့ခ်ျကိုလက်မှတ်ထိုးပါ", + "sign_verify_title": "လက်မှတ်ထိုး / အတည်ပြုရန်", + "signature": "လက်မှတ်", + "signature_invalid_error": "အဆိုပါလက်မှတ်ပေးထားသောမက်ဆေ့ခ်ျကိုများအတွက်မမှန်ကန်ပါ", "signTransaction": "ငွေလွှဲဝင်ပါ။", "signup_for_card_accept_terms": "ကတ်အတွက် စာရင်းသွင်းပြီး စည်းကမ်းချက်များကို လက်ခံပါ။", "silent_payments": "အသံတိတ်ငွေပေးချေမှု", @@ -822,6 +829,7 @@ "value_type": "Value အမျိုးအစား", "variable_pair_not_supported": "ရွေးချယ်ထားသော ဖလှယ်မှုများဖြင့် ဤပြောင်းလဲနိုင်သောအတွဲကို ပံ့ပိုးမထားပါ။", "verification": "စိစစ်ခြင်း။", + "verify_message": "မက်ဆေ့ခ်ျကိုအတည်ပြုရန်", "verify_with_2fa": "Cake 2FA ဖြင့် စစ်ဆေးပါ။", "version": "ဗားရှင်း ${currentVersion}", "view_all": "အားလုံးကိုကြည့်ရှုပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 0f6149182..2d55344f5 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Memo:", "message": "Bericht", + "message_verified": "Het bericht is succesvol geverifieerd", "methods": "Methoden", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "Toon zaad/sleutels", "show_market_place": "Toon Marktplaats", "show_seed": "Toon zaad", + "sign_message": "Aanmeldingsbericht", "sign_up": "Aanmelden", + "sign_verify_message": "Teken of verifieer bericht", + "sign_verify_message_sub": "Teken of verifieer een bericht met uw privésleutel", + "sign_verify_title": "Ondertekenen / verifiëren", + "signature": "Handtekening", + "signature_invalid_error": "De handtekening is niet geldig voor het gegeven bericht", "signTransaction": "Transactie ondertekenen", "signup_for_card_accept_terms": "Meld je aan voor de kaart en accepteer de voorwaarden.", "silent_payments": "Stille betalingen", @@ -822,6 +829,7 @@ "value_type": "Waarde type", "variable_pair_not_supported": "Dit variabelenpaar wordt niet ondersteund met de geselecteerde uitwisselingen", "verification": "Verificatie", + "verify_message": "Verifieer bericht", "verify_with_2fa": "Controleer met Cake 2FA", "version": "Versie ${currentVersion}", "view_all": "Alles bekijken", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 48d6e38f6..833fc0308 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Notatka:", "message": "Wiadomość", + "message_verified": "Wiadomość została pomyślnie zweryfikowana", "methods": "Metody", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "Pokaż seed/klucze", "show_market_place": "Pokaż rynek", "show_seed": "Pokaż frazy seed", + "sign_message": "Podpisuj wiadomość", "sign_up": "Zarejestruj się", + "sign_verify_message": "Podpisz lub zweryfikuj wiadomość", + "sign_verify_message_sub": "Podpisz lub zweryfikuj wiadomość za pomocą klucza prywatnego", + "sign_verify_title": "Podpisać / weryfikować", + "signature": "Podpis", + "signature_invalid_error": "Podpis nie jest ważny dla podanej wiadomości", "signTransaction": "Podpisz transakcję", "signup_for_card_accept_terms": "Zarejestruj się, aby otrzymać kartę i zaakceptuj warunki.", "silent_payments": "Ciche płatności", @@ -822,6 +829,7 @@ "value_type": "Typ wartości", "variable_pair_not_supported": "Ta para zmiennych nie jest obsługiwana na wybranych giełdach", "verification": "Weryfikacja", + "verify_message": "Sprawdź wiadomość", "verify_with_2fa": "Sprawdź za pomocą Cake 2FA", "version": "Wersja ${currentVersion}", "view_all": "Wyświetl wszystko", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 070f3f776..bd6d9b506 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -371,6 +371,7 @@ "max_value": "Máx: ${value} ${currency}", "memo": "Memorando:", "message": "Mensagem", + "message_verified": "A mensagem foi verificada com sucesso", "methods": "Métodos", "min_amount": "Mínimo: ${valor}", "min_value": "Mín: ${value} ${currency}", @@ -663,7 +664,13 @@ "show_keys": "Mostrar semente/chaves", "show_market_place": "Mostrar mercado", "show_seed": "Mostrar semente", + "sign_message": "Mensagem de assinar", "sign_up": "Inscrever-se", + "sign_verify_message": "Assinar ou verificar mensagem", + "sign_verify_message_sub": "Assine ou verifique uma mensagem usando sua chave privada", + "sign_verify_title": "Assinar / verificar", + "signature": "Assinatura", + "signature_invalid_error": "A assinatura não é válida para a mensagem dada", "signTransaction": "Assinar transação", "signup_for_card_accept_terms": "Cadastre-se no cartão e aceite os termos.", "silent_payments": "Pagamentos silenciosos", @@ -824,6 +831,7 @@ "value_type": "Tipo de valor", "variable_pair_not_supported": "Este par de variáveis não é compatível com as trocas selecionadas", "verification": "Verificação", + "verify_message": "Verifique a mensagem", "verify_with_2fa": "Verificar com Cake 2FA", "version": "Versão ${currentVersion}", "view_all": "Ver todos", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 695877c77..d43572351 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -370,6 +370,7 @@ "max_value": "Макс: ${value} ${currency}", "memo": "Памятка:", "message": "Сообщение", + "message_verified": "Сообщение было успешно проверено", "methods": "Методы", "min_amount": "Минимум: ${value}", "min_value": "Мин: ${value} ${currency}", @@ -662,7 +663,13 @@ "show_keys": "Показать мнемоническую фразу/ключи", "show_market_place": "Показать торговую площадку", "show_seed": "Показать мнемоническую фразу", + "sign_message": "Сообщение о знаке", "sign_up": "Зарегистрироваться", + "sign_verify_message": "Подписать или проверить сообщение", + "sign_verify_message_sub": "Подписать или проверить сообщение, используя свой закрытый ключ", + "sign_verify_title": "Знак / проверка", + "signature": "Подпись", + "signature_invalid_error": "Подпись недопустима для данного сообщения", "signTransaction": "Подписать транзакцию", "signup_for_card_accept_terms": "Подпишитесь на карту и примите условия.", "silent_payments": "Молчаливые платежи", @@ -823,6 +830,7 @@ "value_type": "Тип значения", "variable_pair_not_supported": "Эта пара переменных не поддерживается выбранными биржами.", "verification": "Проверка", + "verify_message": "Проверьте сообщение", "verify_with_2fa": "Подтвердить с помощью Cake 2FA", "version": "Версия ${currentVersion}", "view_all": "Просмотреть все", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 5757eed0b..d948ba6d6 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -370,6 +370,7 @@ "max_value": "ขั้นสูง: ${value} ${currency}", "memo": "หมายเหตุ:", "message": "ข้อความ", + "message_verified": "ข้อความได้รับการตรวจสอบอย่างประสบความสำเร็จ", "methods": "วิธีการ", "min_amount": "จำนวนขั้นต่ำ: ${value}", "min_value": "ขั้นต่ำ: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "แสดงซีด/คีย์", "show_market_place": "แสดงตลาดกลาง", "show_seed": "แสดงซีด", + "sign_message": "ลงนามข้อความ", "sign_up": "สมัครสมาชิก", + "sign_verify_message": "ลงชื่อเข้าใช้หรือตรวจสอบข้อความ", + "sign_verify_message_sub": "ลงชื่อเข้าใช้หรือตรวจสอบข้อความโดยใช้คีย์ส่วนตัวของคุณ", + "sign_verify_title": "ลงชื่อ / ตรวจสอบ", + "signature": "ลายเซ็น", + "signature_invalid_error": "ลายเซ็นไม่ถูกต้องสำหรับข้อความที่ให้ไว้", "signTransaction": "ลงนามในการทำธุรกรรม", "signup_for_card_accept_terms": "ลงทะเบียนสำหรับบัตรและยอมรับเงื่อนไข", "silent_payments": "การชำระเงินเงียบ", @@ -822,6 +829,7 @@ "value_type": "ประเภทค่า", "variable_pair_not_supported": "คู่ความสัมพันธ์ที่เปลี่ยนแปลงได้นี้ไม่สนับสนุนกับหุ้นที่เลือก", "verification": "การตรวจสอบ", + "verify_message": "ยืนยันข้อความ", "verify_with_2fa": "ตรวจสอบกับ Cake 2FA", "version": "เวอร์ชัน ${currentVersion}", "view_all": "ดูทั้งหมด", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 7fc83afd8..a0ab030c8 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -370,6 +370,7 @@ "max_value": "Max: ${value} ${currency}", "memo": "Memo:", "message": "Mensahe", + "message_verified": "Ang mensahe ay matagumpay na na -verify", "methods": "Mga Paraan", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", @@ -660,6 +661,12 @@ "show_details": "Ipakita ang mga detalye", "show_keys": "Ipakita ang mga seed/key", "show_market_place": "Ipakita ang Marketplace", + "sign_message": "Mag -sign Message", + "sign_verify_message": "Mag -sign o i -verify ang mensahe", + "sign_verify_message_sub": "Mag -sign o i -verify ang isang mensahe gamit ang iyong pribadong key", + "sign_verify_title": "Mag -sign / Mag -verify", + "signature": "Lagda", + "signature_invalid_error": "Ang lagda ay hindi wasto para sa ibinigay na mensahe", "show_seed": "Ipakita ang seed", "sign_up": "Mag-sign Up", "signTransaction": "Mag-sign ang Transaksyon", @@ -820,6 +827,7 @@ "use_testnet": "Gumamit ng testnet", "value": "Halaga", "value_type": "Uri ng halaga", + "verify_message": "I -verify ang mensahe", "variable_pair_not_supported": "Ang variable na pares na ito ay hindi suportado sa mga napiling exchange", "verification": "Pag-verify", "verify_with_2fa": "Mag-verify sa Cake 2FA", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 4d1aa43e4..e7cdc5b12 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -370,6 +370,7 @@ "max_value": "En fazla: ${value} ${currency}", "memo": "Memo:", "message": "İleti", + "message_verified": "Mesaj başarıyla doğrulandı", "methods": "Yöntemler", "min_amount": "Min: ${value}", "min_value": "En az: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "Tohumları/anahtarları göster", "show_market_place": "Pazar Yerini Göster", "show_seed": "Tohumları göster", + "sign_message": "İşaret mesajı", "sign_up": "Kaydol", + "sign_verify_message": "Mesajı işaretleyin veya doğrulayın", + "sign_verify_message_sub": "Özel anahtarınızı kullanarak bir mesajı imzalayın veya doğrulayın", + "sign_verify_title": "İşaretle / Doğrula", + "signature": "İmza", + "signature_invalid_error": "İmza verilen mesaj için geçerli değil", "signTransaction": "İşlem İmzala", "signup_for_card_accept_terms": "Kart için kaydol ve koşulları kabul et.", "silent_payments": "Sessiz ödemeler", @@ -822,6 +829,7 @@ "value_type": "Değer türü", "variable_pair_not_supported": "Bu değişken paritesi seçilen borsalarda desteklenmemekte", "verification": "Doğrulama", + "verify_message": "Mesajı Doğrula", "verify_with_2fa": "Cake 2FA ile Doğrulayın", "version": "Sürüm ${currentVersion}", "view_all": "Hepsini göster", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 22edec0d5..91b5b5266 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -370,6 +370,7 @@ "max_value": "Макс: ${value} ${currency}", "memo": "Пам’ятка:", "message": "повідомлення", + "message_verified": "Повідомлення було успішно перевірено", "methods": "методи", "min_amount": "Мінімум: ${value}", "min_value": "Мін: ${value} ${currency}", @@ -662,7 +663,13 @@ "show_keys": "Показати мнемонічну фразу/ключі", "show_market_place": "Відображати маркетплейс", "show_seed": "Показати мнемонічну фразу", + "sign_message": "Підпишіть повідомлення", "sign_up": "Зареєструватися", + "sign_verify_message": "Підпишіть або перевірити повідомлення", + "sign_verify_message_sub": "Підпишіть або перевірте повідомлення за допомогою вашого приватного ключа", + "sign_verify_title": "Знак / Перевірка", + "signature": "Підпис", + "signature_invalid_error": "Підпис не є дійсним для наведеного повідомлення", "signTransaction": "Підписати транзакцію", "signup_for_card_accept_terms": "Зареєструйтеся на картку та прийміть умови.", "silent_payments": "Мовчазні платежі", @@ -823,6 +830,7 @@ "value_type": "Тип значення", "variable_pair_not_supported": "Ця пара змінних не підтримується вибраними біржами", "verification": "Перевірка", + "verify_message": "Перевірте повідомлення", "verify_with_2fa": "Перевірте за допомогою Cake 2FA", "version": "Версія ${currentVersion}", "view_all": "Переглянути все", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 590f344d8..e54a0db34 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -370,6 +370,7 @@ "max_value": "زیادہ سے زیادہ: ${value} ${currency}", "memo": "میمو:", "message": "ﻡﺎﻐﯿﭘ", + "message_verified": "پیغام کی کامیابی کے ساتھ تصدیق کی گئی", "methods": "ﮯﻘﯾﺮﻃ", "min_amount": "کم سے کم: ${value}", "min_value": "کم سے کم: ${value} ${currency}", @@ -663,7 +664,13 @@ "show_keys": "بیج / چابیاں دکھائیں۔", "show_market_place": "بازار دکھائیں۔", "show_seed": "بیج دکھائیں۔", + "sign_message": "سائن پیغام", "sign_up": "سائن اپ", + "sign_verify_message": "پیغام پر دستخط کریں یا تصدیق کریں", + "sign_verify_message_sub": "اپنی نجی کلید کا استعمال کرتے ہوئے کسی پیغام پر دستخط کریں یا اس کی تصدیق کریں", + "sign_verify_title": "سائن / تصدیق کریں", + "signature": "دستخط", + "signature_invalid_error": "دستخط دیئے گئے پیغام کے لئے درست نہیں ہے", "signTransaction": "۔ﮟﯾﺮﮐ ﻂﺨﺘﺳﺩ ﺮﭘ ﻦﯾﺩ ﻦﯿﻟ", "signup_for_card_accept_terms": "کارڈ کے لیے سائن اپ کریں اور شرائط کو قبول کریں۔", "silent_payments": "خاموش ادائیگی", @@ -824,6 +831,7 @@ "value_type": "قدر کی قسم", "variable_pair_not_supported": "یہ متغیر جوڑا منتخب ایکسچینجز کے ساتھ تعاون یافتہ نہیں ہے۔", "verification": "تصدیق", + "verify_message": "پیغام کی تصدیق کریں", "verify_with_2fa": "کیک 2FA سے تصدیق کریں۔", "version": "ورژن ${currentVersion}", "view_all": "سب دیکھیں", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index e5cf3d3f9..8f47d8543 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -371,6 +371,7 @@ "max_value": "kò gbọ́dọ̀ tóbi ju ${value} ${currency}", "memo": "Àkọsílẹ̀:", "message": "Ifiranṣẹ", + "message_verified": "Ifiranṣẹ naa ni aṣeyọri ni ifijišẹ", "methods": "Awọn ọna", "min_amount": "kò kéré ju: ${value}", "min_value": "kò gbọ́dọ̀ kéré ju ${value} ${currency}", @@ -662,7 +663,13 @@ "show_keys": "Wo hóró / àwọn kọ́kọ́rọ́", "show_market_place": "Wa Sopọ Pataki", "show_seed": "Wo hóró", + "sign_message": "Ifiranṣẹ Ami", "sign_up": "Forúkọ sílẹ̀", + "sign_verify_message": "Ami tabi ṣayẹwo ifiranṣẹ", + "sign_verify_message_sub": "Wọle tabi ṣayẹwo ifiranṣẹ kan nipa lilo bọtini ikọkọ rẹ", + "sign_verify_title": "Ami / Daju", + "signature": "Ibọwọlu", + "signature_invalid_error": "Ibuwọlu ko wulo fun ifiranṣẹ ti a fun", "signTransaction": "Wole Idunadura", "signup_for_card_accept_terms": "Ẹ f'orúkọ sílẹ̀ láti gba káàdì àti àjọrò.", "silent_payments": "Awọn sisanwo ipalọlọ", @@ -823,6 +830,7 @@ "value_type": "Iru iye", "variable_pair_not_supported": "A kì í ṣe k'á fi àwọn ilé pàṣípààrọ̀ yìí ṣe pàṣípààrọ̀ irú owó méji yìí", "verification": "Ìjẹ́rìísí", + "verify_message": "Daju ifiranṣẹ", "verify_with_2fa": "Ṣeẹda pẹlu Cake 2FA", "version": "Àtúnse ${currentVersion}", "view_all": "Wo gbogbo nǹkan kan", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 25024d0ed..c864a529b 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -370,6 +370,7 @@ "max_value": "最大: ${value} ${currency}", "memo": "备忘录:", "message": "信息", + "message_verified": "该消息已成功验证", "methods": "方法", "min_amount": "最小值: ${value}", "min_value": "最小: ${value} ${currency}", @@ -661,7 +662,13 @@ "show_keys": "显示种子/密钥", "show_market_place": "显示市场", "show_seed": "显示种子", + "sign_message": "标志消息", "sign_up": "注册", + "sign_verify_message": "签名或验证消息", + "sign_verify_message_sub": "使用您的私钥签名或验证消息", + "sign_verify_title": "签名 /验证", + "signature": "签名", + "signature_invalid_error": "签名对于给出的消息无效", "signTransaction": "签署交易", "signup_for_card_accept_terms": "注册卡并接受条款。", "silent_payments": "无声付款", @@ -822,6 +829,7 @@ "value_type": "值类型", "variable_pair_not_supported": "所选交易所不支持此变量对", "verification": "验证", + "verify_message": "验证消息", "verify_with_2fa": "用 Cake 2FA 验证", "version": "版本 ${currentVersion}", "view_all": "查看全部", From 7c9b72483a66e113db6fee8c23d3b8616c76c41f Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Sat, 17 Aug 2024 19:28:45 -0400 Subject: [PATCH 043/142] Socket null handling (#1610) * return null if in connection failure state * reconnect on connection failure * better connection handling * probably not necessary but just incase * connection handling updates * add cancelOnError: true --- cw_bitcoin/lib/electrum.dart | 54 +++++++++++++++++------------ cw_bitcoin/lib/electrum_wallet.dart | 22 +++++------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 69b07d7c1..28043d0c0 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -66,6 +66,7 @@ class ElectrumClient { try { await socket?.close(); + socket = null; } catch (_) {} try { @@ -90,33 +91,40 @@ class ElectrumClient { } _setConnectionStatus(ConnectionStatus.connected); - socket!.listen((Uint8List event) { - try { - final msg = utf8.decode(event.toList()); - final messagesList = msg.split("\n"); - for (var message in messagesList) { - if (message.isEmpty) { - continue; + socket!.listen( + (Uint8List event) { + try { + final msg = utf8.decode(event.toList()); + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + _parseResponse(message); } - _parseResponse(message); + } catch (e) { + print(e.toString()); } - } catch (e) { - print(e.toString()); - } - }, onError: (Object error) { - final errorMsg = error.toString(); - print(errorMsg); - unterminatedString = ''; + }, + onError: (Object error) { + socket = null; + final errorMsg = error.toString(); + print(errorMsg); + unterminatedString = ''; - final currentHost = socket?.address.host; - final isErrorForCurrentHost = errorMsg.contains(" ${currentHost} "); + final currentHost = socket?.address.host; + final isErrorForCurrentHost = errorMsg.contains(" ${currentHost} "); - if (currentHost != null && isErrorForCurrentHost) - _setConnectionStatus(ConnectionStatus.failed); - }, onDone: () { - unterminatedString = ''; - if (host == socket?.address.host) _setConnectionStatus(ConnectionStatus.disconnected); - }); + if (currentHost != null && isErrorForCurrentHost) + _setConnectionStatus(ConnectionStatus.failed); + }, + onDone: () { + socket = null; + unterminatedString = ''; + if (host == socket?.address.host) _setConnectionStatus(ConnectionStatus.disconnected); + }, + cancelOnError: true, + ); keepAlive(); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 8f2360f26..b8d9adb14 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -218,10 +218,7 @@ abstract class ElectrumWalletBase if (electrumClient.isConnected) { syncStatus = SyncedSyncStatus(); } else { - if (electrumClient.uri != null) { - await electrumClient.connectToUri(electrumClient.uri!, useSSL: electrumClient.useSSL); - startSync(); - } + syncStatus = NotConnectedSyncStatus(); } } } @@ -265,6 +262,7 @@ abstract class ElectrumWalletBase Future? _isolate; void Function(FlutterErrorDetails)? _onError; + Timer? _reconnectTimer; Timer? _autoSaveTimer; static const int _autoSaveInterval = 30; @@ -1980,13 +1978,6 @@ abstract class ElectrumWalletBase break; case ConnectionStatus.failed: syncStatus = LostConnectionSyncStatus(); - // wait for 5 seconds and then try to reconnect: - Future.delayed(Duration(seconds: 5), () { - electrumClient.connectToUri( - node!.uri, - useSSL: node!.useSSL ?? false, - ); - }); break; case ConnectionStatus.connecting: syncStatus = ConnectingSyncStatus(); @@ -1996,7 +1987,11 @@ abstract class ElectrumWalletBase } void _syncStatusReaction(SyncStatus syncStatus) async { - if (syncStatus is NotConnectedSyncStatus) { + if (syncStatus is SyncingSyncStatus) { + return; + } + + if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) { // Needs to re-subscribe to all scripthashes when reconnected _scripthashesUpdateSubject = {}; @@ -2004,7 +1999,8 @@ abstract class ElectrumWalletBase _isTryingToConnect = true; - Future.delayed(Duration(seconds: 10), () { + _reconnectTimer?.cancel(); + _reconnectTimer = Timer(Duration(seconds: 10), () { if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) { this.electrumClient.connectToUri( node!.uri, From c59d39d42d4a8a27208ea3bad5144781801823a5 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sun, 18 Aug 2024 03:37:15 +0300 Subject: [PATCH 044/142] Generic fixes (#1619) * update fee rates * periodically update fees * minor enhancements * minor enhancements * some improvements add solana node * handle empty hex as null * minor improvement * fix imports * fix app hanging on splash screen * update app versions temporarily disable sign/verify for hardware wallets --- assets/solana_node_list.yml | 3 +++ assets/text/Monerocom_Release_Notes.txt | 5 ++--- assets/text/Release_Notes.txt | 7 ++----- cw_bitcoin/lib/electrum_wallet.dart | 12 +++++++++--- cw_evm/lib/evm_chain_wallet.dart | 5 ++++- cw_monero/lib/monero_wallet_service.dart | 6 +++--- cw_monero/pubspec.lock | 15 +++++++-------- cw_monero/pubspec.yaml | 5 +---- cw_wownero/pubspec.lock | 19 +++++++++---------- cw_wownero/pubspec.yaml | 5 +---- lib/core/wallet_loading_service.dart | 8 ++++++-- lib/reactions/bootstrap.dart | 2 +- .../on_authentication_state_change.dart | 17 ++++------------- .../screens/dashboard/pages/balance_page.dart | 4 ++-- .../dashboard/dashboard_view_model.dart | 4 +++- .../exchange/exchange_view_model.dart | 9 +++++++++ macos/Podfile.lock | 18 ++++++------------ pubspec_base.yaml | 7 ++----- scripts/android/app_env.sh | 8 ++++---- scripts/ios/app_env.sh | 8 ++++---- scripts/linux/app_env.sh | 4 ++-- scripts/macos/app_env.sh | 8 ++++---- scripts/windows/build_exe_installer.iss | 2 +- 23 files changed, 89 insertions(+), 92 deletions(-) diff --git a/assets/solana_node_list.yml b/assets/solana_node_list.yml index 4a2e12161..e3ff9138e 100644 --- a/assets/solana_node_list.yml +++ b/assets/solana_node_list.yml @@ -1,4 +1,7 @@ - uri: rpc.ankr.com is_default: true + useSSL: true +- + uri: api.mainnet-beta.solana.com:443 useSSL: true \ No newline at end of file diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index c90d54524..11a3ad803 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,4 +1,3 @@ -Monero synchronization improvements -Enhance error handling -UI enhancements +Scan and verify messages +Synchronization enhancements Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 34bca2e5e..11a3ad803 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,6 +1,3 @@ -Wallets enhancements -Monero synchronization improvements -Improve wallet backups -Enhance error handling -UI enhancements +Scan and verify messages +Synchronization enhancements Bug fixes \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index b8d9adb14..2d0b989d7 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -264,7 +264,8 @@ abstract class ElectrumWalletBase void Function(FlutterErrorDetails)? _onError; Timer? _reconnectTimer; Timer? _autoSaveTimer; - static const int _autoSaveInterval = 30; + Timer? _updateFeeRateTimer; + static const int _autoSaveInterval = 1; Future init() async { await walletAddresses.init(); @@ -272,7 +273,7 @@ abstract class ElectrumWalletBase await save(); _autoSaveTimer = - Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); + Timer.periodic(Duration(minutes: _autoSaveInterval), (_) async => await save()); } @action @@ -425,6 +426,10 @@ abstract class ElectrumWalletBase await updateTransactions(); await updateAllUnspents(); await updateBalance(); + updateFeeRates(); + + _updateFeeRateTimer ??= + Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); if (alwaysScan == true) { _setListeners(walletInfo.restoreHeight); @@ -1213,6 +1218,7 @@ abstract class ElectrumWalletBase await electrumClient.close(); } catch (_) {} _autoSaveTimer?.cancel(); + _updateFeeRateTimer?.cancel(); } @action @@ -1371,7 +1377,7 @@ abstract class ElectrumWalletBase if (confirmations > 0) return false; - if (transactionHex == null) { + if (transactionHex == null || transactionHex.isEmpty) { return false; } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index bbf972f0d..0ade2215e 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -113,6 +113,8 @@ abstract class EVMChainWalletBase int? gasBaseFee = 0; int estimatedGasUnits = 0; + Timer? _updateFeesTimer; + bool _isTransactionUpdating; // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter @@ -263,6 +265,7 @@ abstract class EVMChainWalletBase void close() { _client.stop(); _transactionsUpdateTimer?.cancel(); + _updateFeesTimer?.cancel(); } @action @@ -297,7 +300,7 @@ abstract class EVMChainWalletBase await _updateEstimatedGasFeeParams(); - Timer.periodic(const Duration(seconds: 10), (timer) async { + _updateFeesTimer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { await _updateEstimatedGasFeeParams(); }); diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index d771d1815..f9973f430 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -119,7 +119,7 @@ class MoneroWalletService extends WalletService< } @override - Future openWallet(String name, String password) async { + Future openWallet(String name, String password, {bool? retryOnFailure}) async { MoneroWallet? wallet; try { final path = await pathForWallet(name: name, type: getType()); @@ -181,12 +181,12 @@ class MoneroWalletService extends WalletService< wallet.onError != null) { wallet.onError!(FlutterErrorDetails(exception: e, stack: s)); } - if (invalidPassword) { + if (invalidPassword || retryOnFailure == false) { rethrow; } await restoreOrResetWalletFiles(name); - return openWallet(name, password); + return openWallet(name, password, retryOnFailure: false); } } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 51efd4076..13c70cfad 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -295,10 +295,10 @@ packages: dependency: transitive description: name: hashlib - sha256: "5037d3b8c36384c03a728543ae67d962a56970c5432a50862279fe68ee4c8411" + sha256: d41795742c10947930630118c6836608deeb9047cd05aee32d2baeb697afd66a url: "https://pub.dev" source: hosted - version: "1.19.1" + version: "1.19.2" hashlib_codecs: dependency: transitive description: @@ -575,12 +575,11 @@ packages: polyseed: dependency: "direct main" description: - path: "." - ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f - resolved-ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f - url: "https://github.com/mrcyjanek/polyseed_dart" - source: git - version: "0.0.5" + name: polyseed + sha256: "11d4dbee409db053c5e9cd77382b2f5115f43fc2529158a826a96f3ba505d770" + url: "https://pub.dev" + source: hosted + version: "0.0.6" pool: dependency: transitive description: diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 7f5da2621..3053977de 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -19,10 +19,7 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.18.0 encrypt: ^5.0.1 - polyseed: - git: - url: https://github.com/mrcyjanek/polyseed_dart - ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f + polyseed: ^0.0.6 cw_core: path: ../cw_core monero: diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index f164b3086..58831085f 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -295,18 +295,18 @@ packages: dependency: transitive description: name: hashlib - sha256: "71bf102329ddb8e50c8a995ee4645ae7f1728bb65e575c17196b4d8262121a96" + sha256: d41795742c10947930630118c6836608deeb9047cd05aee32d2baeb697afd66a url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.19.2" hashlib_codecs: dependency: transitive description: name: hashlib_codecs - sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626" + sha256: "2b570061f5a4b378425be28a576c1e11783450355ad4345a19f606ff3d96db0f" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.5.0" hive: dependency: transitive description: @@ -567,12 +567,11 @@ packages: polyseed: dependency: "direct main" description: - path: "." - ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f - resolved-ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f - url: "https://github.com/mrcyjanek/polyseed_dart" - source: git - version: "0.0.5" + name: polyseed + sha256: "11d4dbee409db053c5e9cd77382b2f5115f43fc2529158a826a96f3ba505d770" + url: "https://pub.dev" + source: hosted + version: "0.0.6" pool: dependency: transitive description: diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index eb14286a9..b9306711d 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -19,10 +19,7 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.18.0 encrypt: ^5.0.1 - polyseed: - git: - url: https://github.com/mrcyjanek/polyseed_dart - ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f + polyseed: ^0.0.6 cw_core: path: ../cw_core monero: diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 2b570f14c..0087b1332 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -60,7 +60,9 @@ class WalletLoadingService { String corruptedWalletsSeeds = "Corrupted wallets seeds (if retrievable, empty otherwise):"; try { corruptedWalletsSeeds += await _getCorruptedWalletSeeds(name, type); - } catch (_) {} + } catch (e) { + corruptedWalletsSeeds += "\nFailed to fetch $name seeds: $e"; + } // try opening another wallet that is not corrupted to give user access to the app final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); @@ -90,7 +92,9 @@ class WalletLoadingService { if (!corruptedWalletsSeeds.contains(seeds)) { corruptedWalletsSeeds += seeds; } - } catch (_) {} + } catch (e) { + corruptedWalletsSeeds += "\nFailed to fetch $name seeds: $e"; + } } } diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index 5b1a0ace7..bf045c0dd 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -27,7 +27,7 @@ Future bootstrap(GlobalKey navigatorKey) async { authenticationStore.installed(); } - await startAuthenticationStateChange(authenticationStore, navigatorKey); + startAuthenticationStateChange(authenticationStore, navigatorKey); startCurrentWalletChangeReaction(appStore, settingsStore, fiatConversionStore); startCurrentFiatChangeReaction(appStore, settingsStore, fiatConversionStore); startCurrentFiatApiModeChangeReaction(appStore, settingsStore, fiatConversionStore); diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 014306b98..1aa0a12c6 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -7,26 +7,17 @@ import 'package:flutter/widgets.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/load_current_wallet.dart'; import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:rxdart/subjects.dart'; ReactionDisposer? _onAuthenticationStateChange; dynamic loginError; -StreamController authenticatedErrorStreamController = StreamController(); +StreamController authenticatedErrorStreamController = BehaviorSubject(); -Future reInitializeStreamController() async { - if (!authenticatedErrorStreamController.isClosed) { - await authenticatedErrorStreamController.close(); - } - - authenticatedErrorStreamController = StreamController(); -} - -Future startAuthenticationStateChange( +void startAuthenticationStateChange( AuthenticationStore authenticationStore, GlobalKey navigatorKey, -) async { - await reInitializeStreamController(); - +) { authenticatedErrorStreamController.stream.listen((event) { if (authenticationStore.state == AuthenticationState.allowed) { ExceptionHandler.showError(event.toString(), delayInSeconds: 3); diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 770cda6f9..57f908986 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -287,8 +287,8 @@ class CryptoBalanceWidget extends StatelessWidget { padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DashBoardRoundedCardWidget( customBorder: 30, - title: "Monero wallet is broken", - subTitle: "Here are the things that are broken:\n - " + title: "This wallet has encountered an issue", + subTitle: "Here are the things that you should note:\n - " +dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") +"\n\nPlease restart your wallet and if it doesn't help contact our support.", onTap: () {}, diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 56a0e061b..d58d7535c 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -32,7 +32,6 @@ import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cake_wallet/wownero/wownero.dart' as wow; import 'package:cryptography/cryptography.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/cake_hive.dart'; @@ -485,6 +484,9 @@ abstract class DashboardViewModelBase with Store { @computed bool get hasSignMessages { + if (wallet.isHardwareWallet) { + return false; + } switch (wallet.type) { case WalletType.monero: case WalletType.litecoin: diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 3e45e8ba1..f2ea8eeb4 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -142,8 +142,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with _bestRate = 0; _calculateBestRate(); }); + + if (isElectrumWallet) { + bitcoin!.updateFeeRates(wallet); + } } + bool get isElectrumWallet => + wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash; + bool _useTorOnly; final Box trades; final ExchangeTemplateStore _exchangeTemplateStore; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index c2f37a3f3..8951a2dd1 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -17,15 +17,13 @@ PODS: - in_app_review (0.2.0): - FlutterMacOS - OrderedSet (5.0.0) - - package_info (0.0.1): - - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - ReachabilitySwift (5.0.0) - - share_plus_macos (0.0.1): + - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter @@ -46,10 +44,9 @@ DEPENDENCIES: - 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`) - - package_info (from `Flutter/ephemeral/.symlinks/plugins/package_info/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sp_scanner (from `Flutter/ephemeral/.symlinks/plugins/sp_scanner/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -77,14 +74,12 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral in_app_review: :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos - package_info: - :path: Flutter/ephemeral/.symlinks/plugins/package_info/macos package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - share_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sp_scanner: @@ -104,11 +99,10 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info: 6eba2fd8d3371dda2d85c8db6fe97488f24b74b2 - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 + share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: 269d96e0ec3173e69156be7239b95182be3b8303 url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 07dc7f5af..0d99c1c8c 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -94,10 +94,7 @@ dependencies: # ref: main socks5_proxy: ^1.0.4 flutter_svg: ^2.0.9 - polyseed: - git: - url: https://github.com/mrcyjanek/polyseed_dart - ref: f9adc68dbf879fefadeae8e86d1c2983f5a2cc3f + polyseed: ^0.0.6 nostr_tools: ^1.0.9 solana: ^0.30.1 bitcoin_base: @@ -105,7 +102,7 @@ dependencies: url: https://github.com/cake-tech/bitcoin_base ref: cake-update-v5 ledger_flutter: ^1.0.1 - hashlib: 1.12.0 + hashlib: ^1.19.2 dev_dependencies: flutter_test: diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index c91f24622..324d2d2ae 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.3" -MONERO_COM_BUILD_NUMBER=97 +MONERO_COM_VERSION="1.16.4" +MONERO_COM_BUILD_NUMBER=98 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.3" -CAKEWALLET_BUILD_NUMBER=224 +CAKEWALLET_VERSION="4.19.4" +CAKEWALLET_BUILD_NUMBER=225 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index e32b3e9f3..1405f7939 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.3" -MONERO_COM_BUILD_NUMBER=95 +MONERO_COM_VERSION="1.16.4" +MONERO_COM_BUILD_NUMBER=96 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.3" -CAKEWALLET_BUILD_NUMBER=262 +CAKEWALLET_VERSION="4.19.4" +CAKEWALLET_BUILD_NUMBER=263 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 729cf376b..bc2965193 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.9.2" -CAKEWALLET_BUILD_NUMBER=30 +CAKEWALLET_VERSION="1.9.4" +CAKEWALLET_BUILD_NUMBER=31 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 2f6d51a93..9487be2c8 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.6.2" -MONERO_COM_BUILD_NUMBER=27 +MONERO_COM_VERSION="1.6.4" +MONERO_COM_BUILD_NUMBER=28 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.12.2" -CAKEWALLET_BUILD_NUMBER=83 +CAKEWALLET_VERSION="1.12.4" +CAKEWALLET_BUILD_NUMBER=84 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index 216f367ca..4d7838723 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.0.4" +#define MyAppVersion "0.0.5" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" From fe4b3e4eb05ca5037500778102e031a67243a9ee Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sun, 18 Aug 2024 03:48:25 +0300 Subject: [PATCH 045/142] push haven fix --- cw_bitcoin/pubspec.lock | 6 +++--- cw_haven/lib/haven_wallet.dart | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 12274c1e6..cdb88946e 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -67,11 +67,11 @@ packages: source: git version: "1.0.1" bitcoin_base: - dependency: "direct main" + dependency: "direct overridden" description: path: "." - ref: cake-update-v4 - resolved-ref: "574486bfcdbbaf978dcd006b46fc8716f880da29" + ref: cake-update-v5 + resolved-ref: ff2b10eb27b0254ce4518d054332d97d77d9b380 url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index c0ecbca68..317d9dc65 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -10,10 +10,8 @@ import 'package:cw_haven/haven_transaction_info.dart'; import 'package:cw_haven/haven_wallet_addresses.dart'; import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_haven/api/structs/pending_transaction.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history; -//import 'package:cw_haven/wallet.dart'; import 'package:cw_haven/api/wallet.dart' as haven_wallet; import 'package:cw_haven/api/transaction_history.dart' as transaction_history; import 'package:cw_haven/api/monero_output.dart'; @@ -123,7 +121,8 @@ abstract class HavenWalletBase login: node.login, password: node.password, useSSL: node.useSSL ?? false, - isLightWallet: false, // FIXME: hardcoded value + isLightWallet: false, + // FIXME: hardcoded value socksProxyAddress: node.socksProxyAddress); haven_wallet.setTrustedDaemon(node.trusted); @@ -419,4 +418,12 @@ abstract class HavenWalletBase @override String get password => _password; + + @override + Future signMessage(String message, {String? address = null}) => + throw UnimplementedError(); + + @override + Future verifyMessage(String message, String signature, {String? address = null}) => + throw UnimplementedError(); } From 4071e460f687d4fe2e72368dfd3ed5a30ca2ca1f Mon Sep 17 00:00:00 2001 From: cyan Date: Sun, 18 Aug 2024 14:25:44 +0200 Subject: [PATCH 046/142] fix universal build (#1621) --- scripts/macos/build_monero_all.sh | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/scripts/macos/build_monero_all.sh b/scripts/macos/build_monero_all.sh index c934b4667..edc8efe81 100755 --- a/scripts/macos/build_monero_all.sh +++ b/scripts/macos/build_monero_all.sh @@ -23,12 +23,14 @@ then done else if [[ "x$1" == "xuniversal" ]]; then - ARCHS=(arm64 x86_64) + ARCHS=(x86_64 arm64) else ARCHS=$(uname -m) fi for COIN in monero wownero; do + MONERO_LIBS="" + WOWNERO_LIBS="" for ARCH in "${ARCHS[@]}"; do if [[ "$ARCH" == "arm64" ]]; then @@ -39,17 +41,19 @@ else HOST="${ARCH}-host-apple-darwin" fi - MONERO_LIBS=" -arch ${ARCH} ${MONEROC_RELEASE_DIR}/${HOST}_libwallet2_api_c.dylib" - WOWNERO_LIBS=" -arch ${ARCH} ${WOWNEROC_RELEASE_DIR}/${HOST}_libwallet2_api_c.dylib" + MONERO_LIBS="$MONERO_LIBS -arch ${ARCH} ${MONEROC_RELEASE_DIR}/${HOST}_libwallet2_api_c.dylib" + WOWNERO_LIBS="$WOWNERO_LIBS -arch ${ARCH} ${WOWNEROC_RELEASE_DIR}/${HOST}_libwallet2_api_c.dylib" - if [[ ! $(uname -m) == $ARCH ]]; then - PRC="arch -${ARCH}" - fi + if [[ ! $(uname -m) == $ARCH ]]; then + PRC="arch -${ARCH}" + else + PRC="" + fi - pushd ../monero_c - $PRC ./build_single.sh ${COIN} ${HOST} $NPROC - unxz -f ./release/${COIN}/${HOST}_libwallet2_api_c.dylib.xz - popd + pushd ../monero_c + $PRC ./build_single.sh ${COIN} ${HOST} $NPROC + unxz -f ./release/${COIN}/${HOST}_libwallet2_api_c.dylib.xz + popd done done fi From 19c600ba10c4d72c984388073b0820e362c12bc6 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sun, 18 Aug 2024 17:15:23 +0300 Subject: [PATCH 047/142] update macos build number minor fixes --- .../desktop_widgets/desktop_wallet_selection_dropdown.dart | 4 ++-- lib/src/screens/dashboard/edit_token_page.dart | 2 +- lib/src/screens/exchange_trade/exchange_trade_page.dart | 2 +- lib/src/screens/new_wallet/new_wallet_page.dart | 2 +- lib/src/screens/root/root.dart | 2 +- lib/src/screens/send/widgets/send_card.dart | 2 +- scripts/macos/app_env.sh | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 94489a945..fd0eecac3 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -200,14 +200,14 @@ class _DesktopWalletSelectionDropDownState extends State { ), contractAddress: _contractAddressController.text, ); - if (context.mounted) { + if (mounted) { Navigator.pop(context); } } diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 4d3334f9f..0766a4562 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -284,7 +284,7 @@ class ExchangeTradeState extends State { if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { + if (mounted) { showPopUp( context: context, builder: (BuildContext popupContext) { diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index cb451c056..471240877 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -95,7 +95,7 @@ class _WalletNameFormState extends State { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { + if (mounted) { showPopUp( context: context, builder: (_) { diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 8ce0ddde9..76c6c5cfd 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -219,7 +219,7 @@ class RootState extends State with WidgetsBindingObserver { void waitForWalletInstance(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { + if (mounted) { _walletReactionDisposer = reaction( (_) => widget.appStore.wallet, (WalletBase? wallet) { diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 0a3de3e58..214d162ed 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -82,7 +82,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin( context: context, builder: (BuildContext context) { diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 9487be2c8..342831d65 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -22,7 +22,7 @@ MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.12.4" -CAKEWALLET_BUILD_NUMBER=84 +CAKEWALLET_BUILD_NUMBER=85 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 5449b94ba42c4e60d8cfd2a55696ee0f288a767a Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 19 Aug 2024 16:06:57 +0200 Subject: [PATCH 048/142] Windows fix (#1624) * CW-488 Fix missing Wownero logo * Temporary fix for windows --- .../desktop_wallet_selection_dropdown.dart | 29 +++++++++---------- lib/store/settings_store.dart | 10 +++++-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index fd0eecac3..0dd3458b6 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -1,5 +1,4 @@ import 'package:another_flushbar/flushbar.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/desktop_dropdown_item.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -8,8 +7,9 @@ import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; -import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; @@ -122,9 +122,7 @@ class _DesktopWalletSelectionDropDownState extends State( @@ -149,9 +147,7 @@ class _DesktopWalletSelectionDropDownState extends State _loadWallet(WalletListItem wallet) async { if (SettingsStoreBase.walletPasswordDirectInput) { - Navigator.of(context).pushNamed( - Routes.walletUnlockLoadable, + Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, arguments: WalletUnlockArguments( callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { if (isAuthenticatedSuccessfully) { auth.close(); setState(() {}); } - }, walletName: wallet.name, + }, + walletName: wallet.name, walletType: wallet.type)); return; } - widget._authService.authenticateAction(context, - onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) { - return; - } + widget._authService.authenticateAction( + context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (!isAuthenticatedSuccessfully) return; try { if (mounted) { diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index df2c7767f..ee145195c 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1549,8 +1549,14 @@ abstract class SettingsStoreBase with Store { final macInfo = await deviceInfoPlugin.macOsInfo; deviceName = macInfo.computerName; } else if (Platform.isWindows) { - final windowsInfo = await deviceInfoPlugin.windowsInfo; - deviceName = windowsInfo.productName; + try { + final windowsInfo = await deviceInfoPlugin.windowsInfo; + deviceName = windowsInfo.productName; + } catch (e) { + print(e); + print('likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged'); + deviceName = "Windows Device"; + } } return deviceName; From 49d2560eb94d97285fe9f594f7643f4c2265006e Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Wed, 21 Aug 2024 15:49:47 -0400 Subject: [PATCH 049/142] fix (#1633) --- cw_bitcoin/lib/electrum_wallet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 2d0b989d7..047e5551d 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1005,7 +1005,7 @@ abstract class ElectrumWalletBase if (estimatedTx.inputPrivKeyInfos.isEmpty) { error += "\nNo private keys generated."; } else { - error += "\nAddress: ${utxo.ownerDetails.address.toAddress()}"; + error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}"; key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) { final elemPubkey = element.privkey.getPublic().toHex(); From c0e0cf02aa582e477d0602156a3b01fb4c9db771 Mon Sep 17 00:00:00 2001 From: Rafael Date: Wed, 21 Aug 2024 19:31:20 -0300 Subject: [PATCH 050/142] Fix testnet (#1632) * fix: failed connection and misc changes * default isTestnet to false --------- Co-authored-by: OmarHatem --- cw_bitcoin/lib/electrum.dart | 6 ++++-- cw_bitcoin/lib/electrum_wallet.dart | 12 +++++++----- cw_core/lib/wallet_base.dart | 2 +- lib/bitcoin/cw_bitcoin.dart | 2 +- lib/view_model/node_list/node_list_view_model.dart | 2 +- lib/view_model/transaction_details_view_model.dart | 2 +- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 28043d0c0..6ac8edd1a 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -119,9 +119,11 @@ class ElectrumClient { _setConnectionStatus(ConnectionStatus.failed); }, onDone: () { - socket = null; unterminatedString = ''; - if (host == socket?.address.host) _setConnectionStatus(ConnectionStatus.disconnected); + if (host == socket?.address.host) { + socket = null; + _setConnectionStatus(ConnectionStatus.disconnected); + } }, cancelOnError: true, ); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 047e5551d..da91a29e4 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -42,7 +42,6 @@ import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; -import 'package:http/http.dart' as http; import 'package:sp_scanner/sp_scanner.dart'; part 'electrum_wallet.g.dart'; @@ -89,7 +88,7 @@ abstract class ElectrumWalletBase } : {}), this.unspentCoinsInfo = unspentCoinsInfo, - this.isTestnet = network == BitcoinNetwork.testnet, + this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, super(walletInfo) { this.electrumClient = electrumClient ?? ElectrumClient(); @@ -182,7 +181,7 @@ abstract class ElectrumWalletBase BasedUtxoNetwork network; @override - bool? isTestnet; + bool isTestnet; bool get hasSilentPaymentsScanning => type == WalletType.bitcoin; @@ -426,7 +425,7 @@ abstract class ElectrumWalletBase await updateTransactions(); await updateAllUnspents(); await updateBalance(); - updateFeeRates(); + await updateFeeRates(); _updateFeeRateTimer ??= Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); @@ -448,6 +447,8 @@ abstract class ElectrumWalletBase final feeRates = await electrumClient.feeRates(network: network); if (feeRates != [0, 0, 0]) { _feeRates = feeRates; + } else if (isTestnet) { + _feeRates = [1, 1, 1]; } } @@ -1944,8 +1945,9 @@ abstract class ElectrumWalletBase Future _setInitialHeight() async { if (_chainTipUpdateSubject != null) return; + _currentChainTip = await getUpdatedChainTip(); + if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) { - await getUpdatedChainTip(); await walletInfo.updateRestoreHeight(_currentChainTip!); } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 14ba898a7..f55816bb7 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -95,5 +95,5 @@ abstract class WalletBase verifyMessage(String message, String signature, {String? address = null}); - bool? isTestnet; + bool isTestnet = false; } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index e87773f97..5a71e3549 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -516,7 +516,7 @@ class CWBitcoin extends Bitcoin { @override bool isTestnet(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.isTestnet ?? false; + return bitcoinWallet.isTestnet; } @override diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index ea1dd574e..2721fd7b3 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -52,7 +52,7 @@ abstract class NodeListViewModelBase with Store { switch (_appStore.wallet!.type) { case WalletType.bitcoin: - if (_appStore.wallet!.isTestnet == true) { + if (_appStore.wallet!.isTestnet) { node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!; } else { node = getBitcoinDefaultElectrumServer(nodes: _nodeSource)!; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index ef6474974..18715e508 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -152,7 +152,7 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.monero: return 'https://monero.com/tx/${txId}'; case WalletType.bitcoin: - return 'https://mempool.space/${wallet.isTestnet == true ? "testnet/" : ""}tx/${txId}'; + return 'https://mempool.space/${wallet.isTestnet ? "testnet/" : ""}tx/${txId}'; case WalletType.litecoin: return 'https://blockchair.com/litecoin/transaction/${txId}'; case WalletType.bitcoinCash: From 3cefe3b28847000d29fa97fb29415c20d51b8605 Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 22 Aug 2024 02:36:11 +0200 Subject: [PATCH 051/142] update setup-java and switch distribution (#1629) --- .github/workflows/cache_dependencies.yml | 5 +++-- .github/workflows/pr_test_build_android.yml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index 902a44a42..c0042bf5c 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -23,9 +23,10 @@ jobs: docker-images: true - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: - java-version: "17.x" + distribution: "temurin" + java-version: "17" - name: Configure placeholder git details run: | git config --global user.email "CI@cakewallet.com" diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 5dbf1610f..b7b2aaa71 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -39,9 +39,10 @@ jobs: docker-images: true - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: - java-version: "17.x" + distribution: "temurin" + java-version: "17" - name: Configure placeholder git details run: | git config --global user.email "CI@cakewallet.com" From 799ddcdc6faa0e094f46bd1c37b068d3d8d721e3 Mon Sep 17 00:00:00 2001 From: Jorge Date: Thu, 22 Aug 2024 02:45:16 +0200 Subject: [PATCH 052/142] Fix Spanish string (#1622) --- res/values/strings_es.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 02d17ac96..c308f045a 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -387,7 +387,7 @@ "nano_current_rep": "Representante actual", "nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerde regresar al navegador después de que se complete su transacción!", "nano_pick_new_rep": "Elija un nuevo representante", - "nanogpt_subtitle": "Todos los modelos más nuevos (GPT-4, Claude). \\ Nno suscripción, pague con cripto.", + "nanogpt_subtitle": "Todos los modelos más nuevos (GPT-4, Claude). \\nSin suscripción, pague con cripto.", "narrow": "Angosto", "new_first_wallet_text": "Mantenga fácilmente su criptomoneda segura", "new_node_testing": "Prueba de nuevos nodos", @@ -894,4 +894,4 @@ "you_will_get": "Convertir a", "you_will_send": "Convertir de", "yy": "YY" -} \ No newline at end of file +} From 92f87a53661b5b105c14e5fc27b6664e76083b5f Mon Sep 17 00:00:00 2001 From: smgthbusr <85361717+smgthbusr@users.noreply.github.com> Date: Thu, 22 Aug 2024 02:47:30 +0200 Subject: [PATCH 053/142] Add Armenian language (#1623) * Add Armenian strings * Add Armenian * Add Armenian flag --- assets/images/flags/arm.png | Bin 0 -> 4846 bytes lib/entities/language_service.dart | 2 + res/values/strings_hy.arb | 897 +++++++++++++++++++++++++++++ 3 files changed, 899 insertions(+) create mode 100644 assets/images/flags/arm.png create mode 100644 res/values/strings_hy.arb diff --git a/assets/images/flags/arm.png b/assets/images/flags/arm.png new file mode 100644 index 0000000000000000000000000000000000000000..7bdabd4ee76f5e86f5aa3c8722a2867cabe683dc GIT binary patch literal 4846 zcmeHLdsGuw8lQlVh))oFu%ID6z{ez$WI~cLK@cLu5}=R+L>KKOGYPYKk^~Y|gvFIA zzN?m2DPpVCg7^Zpt8Enl5sR-y>hV#ndd|YOqQ}RsRz&wsc<=U{J)X1uk8p18H~0R& z-~I0Ie)of6T|!*+XfJ;+2!ci{Vxkg3KN~cE<}mPG^VduN0^JodRiRTTWGlshR4Z!|Gx4j+MA>A0-hsW@MMuWZ>G(NC> zL0=9U^7y0O1QwseLpTC4@JIM4hKMm9%Hks!ieh{etj{Rz zi-=}=vw60*e8E{HB{;Uk?+Llnqxox)SzjpsaK~APl*`!8p_DBCOWqIye;xwT<5ah z-t2ra#rYV@t-7G93TEuzNfL>QprU1duaoJzagpp_8p`C(pC7X}DSqyuNUOh&UEbII6lUJTguFqh48LmUPfI~C+n zq{T+EP!7sLV7ZIR;Oq-UJXOU*dwEh%?w%6a9X*0UF zLuk3Aku(9Q9jwY5uq1usdOc_f^pwf!_5xxLKsqR0pI8H8qi5XP=^qGi@8unU?%BH= z3{XlX7G=?9(%~thWNdnUjId}a0&|-@F@f_?5ef?ggak(UJOUQ;a0C_!gd#+PkOBdY z_oGsn?GD_mC21-^&Y=L0MkLXaJYguT6Z3R{LL`7CT2umSg+f9r2$kSsT-T2x-bR6} z#Et!aQK7MsdqG0NDzRkwFq39b`? zJMbvnK?2g=I;`rc!*dWmhIEBb(`wLpLmb{@h}*WH_DIITJ?aO z$no;`4V%92xaK#iK<;Tj4abOUkL1V;BA%R;sJ5(q;IiDSX%o$SU1q8&-eClp3Mu4q KQ3n@%kpFL7qruVu literal 0 HcmV?d00001 diff --git a/lib/entities/language_service.dart b/lib/entities/language_service.dart index 23d27dd38..bb60c5cce 100644 --- a/lib/entities/language_service.dart +++ b/lib/entities/language_service.dart @@ -30,6 +30,7 @@ class LanguageService { 'yo': 'Yorùbá (Yoruba)', 'ha': 'Hausa Najeriya (Nigeria)', 'tl': 'Filipino (Tagalog)' + 'hy': 'Հայերեն (Armenian)', }; static const Map localeCountryCode = { @@ -59,6 +60,7 @@ class LanguageService { 'yo': 'nga', 'ha': 'hau', 'tl': 'phl' + 'hy': 'arm' }; static final list = {}; diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb new file mode 100644 index 000000000..826bfa99a --- /dev/null +++ b/res/values/strings_hy.arb @@ -0,0 +1,897 @@ +{ + "about_cake_pay": "Cake Pay-ը Ձեզ թույլ է տալիս հեշտությամբ գնել նվեր քարտեր վիրտուալ ակտիվներով, որոնք անմիջապես ծախսվում են ԱՄՆ-ի 150,000-ից ավելի առևտրականների մոտ:", + "account": "Հաշիվ", + "accounts": "Հաշիվներ", + "accounts_subaddresses": "Հաշիվներ և ենթահասցեներ", + "activate": "Ակտիվացնել", + "active": "Ակտիվ", + "active_cards": "Ակտիվ քարտեր", + "activeConnectionsPrompt": "Ակտիվ կապերը կհայտնվեն այստեղ", + "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_receiver": "Ավելացնել ևս մեկ ստացող (ընտրովի)", + "add_secret_code": "Կամ ավելացրեք այս գաղտնի կոդը վավերացնող հավելվածում", + "add_tip": "Ավելացնել Թեյավճար", + "add_token_disclaimer_check": "Ես հաստատել եմ token-ի պայմանագրի հասցեն և տեղեկատվությունը վստահելի աղբյուրի օգտագործմամբ: Վնասակար կամ սխալ տեղեկատվության ավելացումը կարող է հանգեցնել միջոցների կորստի:", + "add_token_warning": "Մի խմբագրեք կամ ավելացրեք token-ներ, ինչպես կոչ են անում խարդախները:\nՄիշտ հաստատեք Token-ների հասցեները վստահելի աղբյուրներով:", + "add_value": "Ավելացնել արժեք", + "address": "Հասցե", + "address_book": "Հասցեագիրք", + "address_book_menu": "Հասցեագիրք", + "address_detected": "Հասցեն հայտնաբերված է", + "address_from_domain": "Այս հասցեն է ${domain} Unstoppable Domains-ից", + "address_from_yat": "Այս հասցեն է ${emoji} Yat-ից", + "address_label": "Հասցեի պիտակ", + "address_remove_contact": "Հեռացնել կոնտակտը", + "address_remove_content": "Վստահ եք, որ ուզում եք հեռացնել ընտրված կոնտակտը?", + "addresses": "Հասցեներ", + "advanced_settings": "Առաջադեմ կարգավորումներ", + "aggressive": "Ագրեսիվ", + "agree": "Համաձայն եմ", + "agree_and_continue": "Համաձայն եմ և շարունակեմ", + "agree_to": "Ստեղծելով հաշիվ դուք համաձայնում եք ", + "all": "Բոլորը", + "all_trades": "Բոլոր գործարքները", + "all_transactions": "Բոլոր գործառնությունները", + "alphabetical": "Այբբենական", + "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": "Հաստատել", + "arrive_in_this_address": "${currency} ${tag}կժամանի այս հասցեում", + "ascending": "Աճող", + "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": "«Հասանելի մնացորդ» կամ «Հաստատված մնացորդ»-ը միջոցներ են, որոնք կարող են անմիջապես ծախսվել։ Եթե միջոցները հայտնվում են ստորին մնացորդում, բայց ոչ վերևի մնացորդում, ապա դուք պետք է մի քանի րոպե սպասեք, մինչև ներգաղթող միջոցները ավելի ցանցի հաստատումներ ստանան։ Հաստատումներ ստանալուց հետո դրանք կարող են ծախսվել։", + "avg_savings": "Միջին խնայողություն", + "awaitDAppProcessing": "Խնդրեմ սպասեք, մինչև դիմումը կավարտի մշակումը։", + "awaiting_payment_confirmation": "Վճարման հաստատման սպասում", + "background_sync_mode": "Հետին պլանի համաժամացման ռեժիմ", + "backup": "Կրկնօրինակ", + "backup_file": "Կրկնօրինակի ֆայլ", + "backup_password": "Կրկնօրինակի գաղտնաբառ", + "balance": "Հաշվեկշիռ", + "balance_page": "Հաշվեկշռի էջ", + "bill_amount": "Հաշիվը", + "billing_address_info": "Եթե խնդրեն հաշվեվճարի հասցե, ապա տրամադրեք ձեր առաքման հասցեն", + "biometric_auth_reason": "Սկանավեք ձեր մատնահետքը նույնականացման համար", + "bitcoin_dark_theme": "Bitcoin մութ տեսք", + "bitcoin_light_theme": "Bitcoin պայծառ տեսք", + "bitcoin_payments_require_1_confirmation": "Bitcoin վճարումները պահանջում են 1 հաստատում, որը կարող է տևել 20 րոպե կամ ավելի: Շնորհակալություն ձեր համբերության համար: Դուք էլ. նամակ կստանաք, երբ վճարումը հաստատվի։", + "block_remaining": "1 Բլոկ է մնացել", + "Blocks_remaining": "${status} Բլոկ է մնացել", + "bluetooth": "Bluetooth", + "bright_theme": "Պայծառ", + "bump_fee": "Բարձրացնել միջնորդավճարը", + "buy": "Գնել", + "buy_alert_content": "Այժմ մենք աջակցում ենք միայն Bitcoin, Ethereum, Litecoin և Monero արժույթների գնմանը: Խնդրում ենք ստեղծել կամ վերածանցել Bitcoin, Ethereum, Litecoin կամ Monero արժույթների հաշիվը։", + "buy_bitcoin": "Գնել Bitcoin", + "buy_now": "Գնել հիմա", + "buy_provider_unavailable": "Տվյալ պահին մատակարարը անհասանելի է։", + "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 Պահպանեք Ձեր պատվերի համարը՝", + "cake_pay_subtitle": "Գնեք համաշխարհային նախավճարային քարտեր և նվեր քարտեր", + "cake_pay_web_cards_subtitle": "Գնեք համաշխարհային նախավճարային քարտեր և նվեր քարտեր", + "cake_pay_web_cards_title": "Cake Pay Վեբ Քարտեր", + "cake_wallet": "Cake Գաղտնապահոց", + "cakepay_prepaid_card": "CakePay Նախավճարային Դեբետային Քարտ", + "camera_consent": "Ձեր տեսախցիկը կօգտագործվի ${provider}-ի կողմից ինքնությունը հաստատելու նպատակով: Խնդրում ենք ծանոթանալ նրանց Գաղտնիության Քաղաքականության հետ:", + "camera_permission_is_required": "Տեսախցիկի թույլտվություն է պահանջվում: \nԽնդրում ենք այն ակտիվացնել հավելվածի կարգավորումներից:", + "cancel": "Չեղարկել", + "card_address": "Հասցե։", + "cardholder_agreement": "Քարտապանի Պայմանագիր", + "cards": "Քարտեր", + "chains": "Շղթաներ", + "change": "Փոփոխել", + "change_backup_password_alert": "Ձեր նախորդ կրկնօրինակ ֆայլերը չեն հասանելի լինի ներմուծել նոր կրկնօրինակի գաղտնաբառով: Նոր կրկնօրինակի գաղտնաբառը կօգտագործվի միայն նոր կրկնօրինակ ֆայլերի համար: Վստահ եք, որ ցանկանում եք փոխել կրկնօրինակի գաղտնաբառը?", + "change_currency": "Փոխել արժույթը", + "change_current_node": "Վստահ եք, որ ցանկանում եք փոխել ընթացիկ հանգույցը ${node}?", + "change_current_node_title": "Փոխել ընթացիկ հանգույցը", + "change_exchange_provider": "Փոխել փոխանակման մատակարարին", + "change_language": "Փոխել լեզուն", + "change_language_to": "Փոխել լեզուն ${language}?", + "change_password": "Փոխել գաղտնաբառը", + "change_rep": "Փոխել ներկայացուցչին", + "change_rep_message": "Վստահ եք, որ ցանկանում եք փոխել ներկայացուցիչներին?", + "change_rep_successful": "Ներկայացուցչի փոփոխությունը հաջողությամբ կատարվեց", + "change_wallet_alert_content": "Ցանկանում եք փոխել ընթացիկ դրամապանակը ${wallet_name}?", + "change_wallet_alert_title": "Փոխել ընթացիկ դրամապանակը", + "choose_account": "Ընտրեք հաշիվը", + "choose_address": "\n\nԽնդրում ենք ընտրեք հասցեն", + "choose_card_value": "Ընտրեք քարտի արժեք", + "choose_derivation": "Ընտրեք դրամապանակի ածանցում", + "choose_from_available_options": "Ընտրեք առկա տարբերակներից", + "choose_one": "Ընտրեք մեկը", + "choose_relay": "Խնդրում ենք ընտրեք փոխանցման կետ", + "choose_wallet_currency": "Խնդրում ենք ընտրեք դրամապանակի արժույթը", + "clear": "Մաքրել", + "clearnet_link": "Բաց ցանցի հղում", + "close": "Փակել", + "coin_control": "Մետաղադրամի վերահսկում (ըստ ցանկության)", + "cold_or_recover_wallet": "Ավելացնել սառը դրամապանակ կամ վերականգնել թղթային դրամապանակ", + "color_theme": "Գույների տեսք", + "commit_transaction_amount_fee": "Հաստատել գործարքը\nՍկզբնական գումար. ${amount}\nՄիջնորդավճար. ${fee}", + "confirm": "Հաստատել", + "confirm_delete_template": "Այս գործողությունը կջնջի այս ձևանմուշը: Ցանկանու՞մ եք շարունակել։", + "confirm_delete_wallet": "Այս գործողությունը կջնջի այս դրամապանակը: Ցանկանու՞մ եք շարունակել։", + "confirm_fee_deduction": "Հաստատեք միջնորդավճար հանումը", + "confirm_fee_deduction_content": "Դուք համաձայն եք միջնորդավճար հանել արդյունքից?", + "confirm_sending": "Հաստատեք ուղարկումը", + "confirm_silent_payments_switch_node": "Ձեր ընթացիկ հանգույցը չի աջակցում Լուռ վճարումներին\nCake Wallet-ը կանցնի համատեղելի հանգույց, միայն սկանավորման համար", + "confirmations": "Հաստատումներ", + "confirmed": "Հաստատված մնացորդ", + "confirmed_tx": "Հաստատված", + "congratulations": "Շնորհավորանք!", + "connect_an_existing_yat": "Միացրեք գոյություն ունեցող Yat-ը", + "connect_yats": "Միացրեք Yat-ները", + "connect_your_hardware_wallet": "Միացրեք ձեր ապարատային դրամապանակը Bluetooth-ի կամ USB-ի միջոցով", + "connect_your_hardware_wallet_ios": "Միացրեք ձեր ապարատային դրամապանակը Bluetooth-ի միջոցով", + "connection_sync": "Կապ և սինխրոնիզացիա", + "connectWalletPrompt": "Միացրեք ձեր դրամապանակը WalletConnect-ի միջոցով գործարքներ կատարելու համար", + "contact": "Կոնտակտ", + "contact_list_contacts": "Կոնտակտներ", + "contact_list_wallets": "Իմ դրամապանակներ", + "contact_name": "Կոնտակտի անուն", + "contact_support": "Հետադարձ կապ", + "continue_text": "Շարունակել", + "contractName": "Գործարքի անուն", + "contractSymbol": "Գործարքի Նշան", + "copied_key_to_clipboard": "${key} պատճենված է տեքստի բուֆերում", + "copied_to_clipboard": "Պատճենված է տեքստի բուֆերում", + "copy": "Պատճենել", + "copy_address": "Պատճենել հասցեն", + "copy_id": "Պատճենել ID", + "copyWalletConnectLink": "Պատճենել WalletConnect հղումը dApp-ից և տեղադրել այստեղ", + "countries": "Երկրներ", + "create_account": "Ստեղծել հաշիվ", + "create_backup": "Ստեղծել կրկնօրինակ", + "create_donation_link": "Ստեղծել նվիրատվության հղում", + "create_invoice": "Ստեղծել հաշիվ-ապրանքագիր", + "create_new": "Ստեղծել նոր դրամապանակ", + "create_new_account": "Ստեղծել նոր հաշիվ", + "creating_new_wallet": "Նոր դրամապանակ ստեղծվում է", + "creating_new_wallet_error": "Սխալ: ${description}", + "creation_date": "Ստեղծման ամսաթիվ", + "custom": "Պատվերով", + "custom_drag": "Պատվերով (Պահել և Գցել)", + "custom_redeem_amount": "Պատվերով Փրկագնման Գումար", + "custom_value": "Պատվերով Արժեք", + "dark_theme": "Մութ", + "debit_card": "Դեբետային քարտ", + "debit_card_terms": "Ձեր վճարային քարտի համարի (և ձեր վճարային քարտի համարի համապատասխան վկայականներ) պահպանումն ու օգտագործումը այս թվային դրամապանակում ենթակա են վճարային քարտ թողարկող կողմի գործող պայմանների և պայմանագրի", + "decimal_places_error": "Խմբակային տեղերի սխալ", + "decimals_cannot_be_zero": "Խմբակային տեղերը չեն կարող լինել զրո", + "default_buy_provider": "Լռելյայն գնման մատակարար", + "default_sell_provider": "Լռելյայն վաճառքի մատակարար", + "delete": "Ջնջել", + "delete_account": "Ջնջել հաշիվը", + "delete_wallet": "Ջնջել դրամապանակը", + "delete_wallet_confirm_message": "Դուք վստահ եք, որ ուզում եք ջնջել ${wallet_name} դրամապանակը?", + "deleteConnectionConfirmationPrompt": "Դուք վստահ եք, որ ուզում եք ջնջել կապը", + "denominations": "Անվանակարգեր", + "descending": "Նվազող", + "description": "Նկարագրություն", + "destination_tag": "Նպատակակետի պիտակ:", + "dfx_option_description": "Գնեք կրիպտոարժույթ EUR և CHF: Կորպորատիվ և մանրածախ հաճախորդների համար Եվրոպայում", + "didnt_get_code": "Չեք ստացել կոդը?", + "digit_pin": "-նիշ ՊԻՆ", + "digital_and_physical_card": " թվային և ֆիզիկական նախավճարային դեբետային քարտ", + "disable": "Անջատել", + "disable_bulletin": "Անջատել ծառայության վիճակի տեղեկագիրը", + "disable_buy": "Անջատել գնում գործողությունը", + "disable_cake_2fa": "Անջատել Cake 2FA", + "disable_exchange": "Անջատել փոխանակումը", + "disable_fiat": "Անջատել ֆիատ", + "disable_sell": "Անջատել վաճառք գործողությունը", + "disableBatteryOptimization": "Անջատել մարտկոցի օպտիմիզացիան", + "disableBatteryOptimizationDescription": "Դուք ցանկանում եք անջատել մարտկոցի օպտիմիզացիան ֆոնային համաժամացման ավելի ազատ և հարթ ընթացքի համար?", + "disabled": "Անջատված", + "discount": "Խնայեք ${value}%", + "display_settings": "Ցուցադրման կարգավորումներ", + "displayable": "Ցուցադրվող", + "do_not_have_enough_gas_asset": "Դուք չունեք բավարար ${currency} տրանզակցիան կատարելու համար ընթացիկ բլոկչեյն ցանցի պայմաններում: Դուք պետք է ունենաք ավելի շատ ${currency} blockchain ցանցի միջնորդավճարները վճարելու համար, նույնիսկ եթե դուք այլ ակտիվ եք ուղարկում:", + "do_not_send": "Մի ուղարկեք", + "do_not_share_warning_text": "Մի կիսեք այս տեղեկատվությունը որևէ մեկի հետ, այդ թվում նաև աջակցության հետ: \n\nՁեր միջոցները կարող են գողանալ կորցնել!", + "do_not_show_me": "Մի ցուցադրեք ինձ նորից", + "domain_looks_up": "Դոմեյնի որոնում", + "donation_link_details": "Նվիրատվության հղումի մանրամասներ", + "e_sign_consent": "Էլեկտրոնային ստորագրության համաձայնություն", + "edit": "Խմբագրել", + "edit_backup_password": "Փոփոխել Կրկնօրինակի Գաղտնաբառը", + "edit_node": "Փոփոխել հանգույց", + "edit_token": "Փոփոխել տոկեն", + "electrum_address_disclaimer": "Մենք ստեղծում ենք նոր հասցե ամեն անգամ, երբ դուք օգտագործում եք այն, բայց նախորդ հասցեները շարունակում են աշխատել", + "email_address": "Էլ. փոստի հասցե", + "enable_replace_by_fee": "Միացնել փոխարինումը միջնորդավճարով", + "enable_silent_payments_scanning": "Միացնել Լուռ Վճարումների սկանավորումը", + "enabled": "Միացված", + "enter_amount": "Մուտքագրեք գումար", + "enter_backup_password": "Մուտքագրեք կրկնօրինակի գաղտնաբառը", + "enter_code": "Մուտքագրեք կոդը", + "enter_seed_phrase": "Մուտքագրեք սերմի արտահայտությունը", + "enter_totp_code": "Մուտքագրեք TOTP կոդը", + "enter_wallet_password": "Մուտքագրեք դրամապանակի գաղտնաբառը", + "enter_your_note": "Մուտքագրեք ձեր նշումը…", + "enter_your_pin": "Մուտքագրեք ձեր PIN-ը", + "enter_your_pin_again": "Մուտքագրեք ձեր PIN-ը կրկին", + "enterTokenID": "Մուտքագրեք Token ID֊ն", + "enterWalletConnectURI": "Մուտքագրեք WalletConnect URI", + "error": "Սխալ", + "error_dialog_content": "Օպս, մենք սխալ ենք ստացել: \n\nԽնդրում ենք ուղարկել սխալի հաշվետվությունը մեր աջակցության թիմին ծրագիրը բարելավելու համար:", + "error_text_account_name": "Հաշվի անունը կարող է պարունակել միայն տառեր և թվեր և պետք է լինի 1-15 նիշ", + "error_text_address": "Դրամապանակի հասցեն պետք է համապատասխանի կրիպտոարժույթի տեսակին", + "error_text_amount": "Գումարը կարող է պարունակել միայն թվեր", + "error_text_contact_name": "Կոնտակտի անունը չի կարող պարունակել ` , '' \" սիմվոլներ և պետք է լինի 1-32 նիշ", + "error_text_crypto_currency": "Կոտորակային մասը պետք է լինի 12 նիշից ցածր կամ հավասար", + "error_text_fiat": "Գումարը չի կարող գերազանցել մնացորդը: Կոտորակային մասը պետք է լինի 2 նիշից ցածր կամ հավասար", + "error_text_input_above_maximum_limit": "Գումարը ավելի է, քան առավելագույնը", + "error_text_input_below_minimum_limit": "Գումարը ցածր է, քան նվազագույնը", + "error_text_keys": "Դրամապանակի բանալիները կարող են պարունակել միայն 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 նիշ հեքսային կոդով", + "error_text_subaddress_name": "Ենթահասցեի անունը չի կարող պարունակել , '' \" նշանները\nև պետք է լինի 1-ից 20 նիշ երկարությամբ", + "error_text_template": "Տեսակի անունը և հասցեն չեն կարող պարունակել , '' \" նշանները\nև պետք է լինի 1-ից 106 նիշ երկարությամբ", + "error_text_wallet_name": "Գաղտնաբառը կարող է պարունակել միայն տառեր, թվեր, _ - նշաններ\nև պետք է լինի 1-ից 33 նիշ երկարությամբ", + "error_text_xmr": "XMR արժեքը չի կարող գերազանցել հասանելի մնացորդը:\nԿոտորակային թվերի քանակը պետք է լինի 12-ից պակաս կամ հավասար", + "errorGettingCredentials": "Սխալ. ծանրաբեռնված վստահագրեր ստանալիս", + "errorSigningTransaction": "Սխալ է տեղի ունեցել գործարքը ստորագրելիս", + "estimated": "Գնահատված", + "estimated_new_fee": "Գնահատված նոր միջնորդավճար", + "estimated_receive_amount": "Գնահատված ստացված գումար", + "etherscan_history": "Etherscan պատմություն", + "event": "Իրադարձություն", + "events": "Իրադարձություններ", + "exchange": "Փոխել", + "exchange_incorrect_current_wallet_for_xmr": "Եթե դուք ցանկանում եք փոխանակել XMR ձեր Cake Wallet Monero հաշվեհամարից, խնդրում ենք անցնել ձեր Monero հաշվեհամարին", + "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": "Խնդրում ենք սպասել մինչև ձեր հաշվեհամարը համաժամացվի", + "expired": "Վավերականությունը լրացել է", + "expires": "Վավերականությունը լրանում է", + "expiresOn": "Վավերականությունը լրանում է", + "expiry_and_validity": "Վավերականություն և լրացում", + "export_backup": "Արտահանել կրկնօրինակը", + "extra_id": "Լրացուցիչ ID", + "extracted_address_content": "Դուք կուղարկեք գումար ${recipient_name}", + "failed_authentication": "Վավերացումը ձախողվեց. ${state_error}", + "faq": "Հաճախ տրվող հարցեր", + "features": "Հատկանիշներ", + "fetching": "Ստացվում է", + "fiat_api": "Fiat API", + "fiat_balance": "Fiat մնացորդ", + "field_required": "Այս դաշտը պարտադիր է", + "fill_code": "Խնդրում ենք լրացնել հաստատման կոդը ձեր էլեկտրոնային փոստում", + "filter_by": "Ֆիլտրել ըստ", + "first_wallet_text": "Հիանալի հաշվեհամար Monero, Bitcoin, Ethereum, Litecoin և Haven արժույթների համար", + "fixed_pair_not_supported": "Այս ֆիքսված զույգը չի ապահովվում ընտրված փոխանակման կետերում", + "fixed_rate": "Ֆիքսված փոխարժեք", + "fixed_rate_alert": "Դուք կկարողանաք մուտքագրել ստացվող գումարը, երբ ֆիքսված փոխարժեքի ռեժիմը միացված է: Դուք ցանկանում եք անցնել ֆիքսված փոխարժեքի ռեժիմին?", + "forgot_password": "Մոռացել եմ գաղտնաբառը", + "freeze": "Կասեցնել", + "frequently_asked_questions": "Հաճախ տրվող հարցեր", + "frozen": "Կասեցված", + "full_balance": "Լրիվ մնացորդ", + "generate_name": "Գեներացնել անուն", + "generating_gift_card": "Գեներացնում է նվեր քարտ", + "get_a": "Ստանալ ", + "get_card_note": " որը կարող եք լրացնել թվային արժույթներով: Հավելյալ տեղեկատվություն պետք չէ!", + "get_your_yat": "Ստանալ ձեր Yat", + "gift_card_amount": "Նվեր քարտի չափ", + "gift_card_balance_note": "Նվեր քարտերը մնացորդով կհայտնվեն այստեղ", + "gift_card_is_generated": "Նվեր քարտ գեներացված է", + "gift_card_number": "Նվեր քարտի համար", + "gift_card_redeemed_note": "Նվեր քարտերը, որ դուք գնել եք, կհայտնվեն այստեղ", + "gift_cards": "Նվեր քարտեր", + "gift_cards_unavailable": "Նվեր քարտերը հասանելի են միայն Monero, Bitcoin և Litecoin արժույթների համար", + "got_it": "Հասկացա", + "gross_balance": "Ընդհանուր մնացորդ", + "group_by_type": "Խմբավորել ըստ տեսակի", + "haven_app": "Haven ծրագիր", + "haven_app_wallet_text": "Հիանալի հաշվեհամար Haven համար", + "help": "Օգնություն", + "hidden_balance": "Թաքնված մնացորդ", + "hide_details": "Թաքցնել մանրամասները", + "high_contrast_theme": "Բարձր հակադրության տեսք", + "home_screen_settings": "Գլխավոր էկրանի կարգավորումներ", + "how_to_use": "Ինչպես օգտագործել", + "how_to_use_card": "Ինչպես օգտագործել այս քարտը", + "id": "ID: ", + "ignor": "Անտեսել", + "import": "Ներմուծել", + "importNFTs": "Ներմուծել NFT-ներ", + "in_store": "Տեղում", + "incoming": "Մուտքային", + "incorrect_seed": "Տեքստը սխալ է", + "inputs": "Մուտքեր", + "insufficient_lamport_for_tx": "Դուք չունեք բավարար SOL՝ գործարքն և գործարքի վարձը ծածկելու համար։ Խնդրում ենք ավելացնել ավելի շատ SOL ձեր դրամապանակում կամ նվազեցնել ուղարկվող SOL-ի քանակը։", + "insufficient_lamports": "Դուք չունեք բավարար SOL՝ գործարքն և գործարքի վարձը ծածկելու համար։ Ձեզ անհրաժեշտ է առնվազն ${solValueNeeded} SOL։ Խնդրում ենք ավելացնել ավելի շատ SOL ձեր դրամապանակում կամ նվազեցնել ուղարկվող SOL-ի քանակը։", + "insufficientFundsForRentError": "Ձեր մնացորդը բավարար չէ վարձակալության համար: Խնդրում ենք ավելացնել մնացորդը կամ նվազեցնել ուղարկվող գումարը", + "introducing_cake_pay": "Ներկայացնում ենք Cake Pay!", + "invalid_input": "Սխալ մուտք", + "invalid_password": "Սխալ գաղտնաբառ", + "invoice_details": "Հաշիվ-ապրանքագրի մանրամասներ", + "is_percentage": "կազմում է", + "last_30_days": "Վերջին 30 օրը", + "learn_more": "Տեղեկանալ ավելին", + "ledger_connection_error": "Չկարողացանք կապ հաստատել Ledger-ի հետ: Խնդրում ենք փորձել նորից", + "ledger_error_device_locked": "Ledger-ը արգելափակված է", + "ledger_error_tx_rejected_by_user": "Գործարքը մերժված է օգտատերի կողմից", + "ledger_error_wrong_app": "Խնդրում ենք համոզվել, որ դուք բացել եք ճիշտ ծրագիրը ձեր Ledger-ում", + "ledger_please_enable_bluetooth": "Խնդրում ենք միացնել Bluetooth-ը ձեր Ledger-ը հայտնաբերելու համար", + "light_theme": "Լուսավոր", + "load_more": "Բեռնել ավելին", + "loading_your_wallet": "Ձեր հաշվեհամարը բեռնում է", + "login": "Մուտք", + "logout": "Ելք", + "low_fee": "Ցածր միջնորդավճար", + "low_fee_alert": "Դուք ներկայումս օգտագործում եք ցածր ցանցային միջնորդավճար: Սա կարող է հանգեցնել երկար սպասելու ժամանակի, տարբեր փոխարժեքների կամ չեղարկված առևտրի: Մենք առաջարկում ենք սահմանել ավելի բարձր միջնորդավճար ավելի լավ փորձ ունենալու համար", + "manage_nodes": "Կարգավորել հանգույցները", + "manage_pow_nodes": "Կարգավորել PoW հանգույցները", + "manage_yats": "Կառավարել Yats", + "mark_as_redeemed": "Նշել որպես մարված", + "market_place": "Շուկա", + "matrix_green_dark_theme": "Matrix Կանաչ Մութ տեսք", + "max_amount": "Առավելագույն գումար: ${value}", + "max_value": "Առավելագույն արժեք: ${value} ${currency}", + "memo": "Մեմո:", + "message": "Հաղորդագրություն", + "message_verified": "Հաղորդագրությունը հաջողությամբ հաստատվեց", + "methods": "Մեթոդներ", + "min_amount": "Նվազը: ${value}", + "min_value": "Նվազը: ${value} ${currency}", + "minutes_to_pin_code": "${minute} րոպե", + "mm": "ԱԱ", + "modify_2fa": "Փոփոխել Cake 2FA", + "monero_com": "Monero.com by Cake Wallet", + "monero_com_wallet_text": "Անհավանական դրամապանակ Monero-ի համար", + "monero_dark_theme": "Monero մութ տեսք", + "monero_light_theme": "Monero պայծառ տեսք", + "moonpay_alert_text": "Գումարի արժեքը պետք է լինի հավասար կամ ավելի քան ${minAmount} ${fiatCurrency}", + "more_options": "Այլ տարբերակներ", + "name": "Անուն", + "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_wallet": "Նոր դրամապանակ", + "newConnection": "Նոր կապ", + "no_cards_found": "Ոչ մի քարտ չի գտնվել", + "no_id_needed": "Ոչ մի փաստաթուղթ չի պահանջվում!", + "no_id_required": "Ոչ մի փաստաթուղթ չի պահանջվում։ Լրացրեք և ծախսեք ամենուր", + "no_relay_on_domain": "Տիրույթի համար ընդունող չկա կամ անհասանելի է։ Խնդրում ենք ընտրել ընդունող", + "no_relays": "Ընդունողներ չկան", + "no_relays_message": "Մենք գտել ենք Nostr NIP-05 գրառում այս օգտատիրոջ համար, բայց այն չի պարունակում ոչ մի ընդունող։ Խնդրում ենք հրահանգել ստացողին ավելացնել ընդունողներ իր Nostr գրառման մեջ", + "node_address": "Հանգույցի հասցե", + "node_connection_failed": "Հանգույցի միացումը ձախողվեց", + "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": "Նշում (սեղմեք փոխելու համար)", + "nullURIError": "URI-ն դատարկ է", + "offer_expires_in": "Առաջարկը վաղեմության է հասնում ", + "offline": "Անցանց", + "ok": "Լավ", + "old_fee": "Հին վճար", + "onion_link": "Onion հղում", + "online": "Առցանց", + "onramper_option_description": "Արագ գնեք կրիպտոցուլեր շատ վճարման մեթոդներով։ Հասանելի է մեծ մասամբ երկրներում։ Տարածված և վճարները փոփոխվում են", + "open_gift_card": "Բացեք նվեր քարտ", + "optional_description": "Ոչ պարտադիր նկարագրություն", + "optional_email_hint": "Ոչ պարտադիր վճարողի ծանուցման էլեկտրոնային փոստ", + "optional_name": "Ոչ պարտադիր ստացողի անուն", + "optionally_order_card": "Ոչ պարտադիր պատվեր ֆիզիկական քարտ", + "orbot_running_alert": "Խնդրում ենք համոզվել, որ Orbot-ը աշխատում է այս հանգույցին միանալուց առաջ", + "order_by": "Պատվեր ըստ", + "order_id": "Պատվերի հերթական համար", + "order_physical_card": "Պատվիրել ֆիզիկական քարտ", + "other_settings": "Այլ կարգավորումներ", + "outdated_electrum_wallet_description": "Cake-ում ստեղծված նոր Bitcoin դրամապանակները ներկայում 24 բառանոց սերմ ունեն։ Դուք պետք է ստեղծեք նոր Bitcoin դրամապանակ և տեղափոխեք բոլոր միջոցները նոր 24 բառանոց դրամապանակ, և դադարեցրեք օգտագործել 12 բառանոց դրամապանակներ", + "outdated_electrum_wallet_receive_warning": "Եթե այս դրամապանակը 12 բառանոց սերմ ունի և ստեղծվել է Cake-ում, Bitcoin մուծել այս դրամապանակ մի տեղափոխեք։ Որեւէ Bitcoin այս դրամապանակ տեղափոխվել կարող է կորած լինել։ Ստեղծեք նոր 24-բառանոց Bitcoin դրամապանակ (սեղմեք վերևի աջ մենյուն, ընտրեք Դրամապանակներ, ընտրեք Նոր Դրամապանակ, ապա ընտրեք Bitcoin) և տեղափոխեք ձեր ԲԻՏԿ-ն այնտեղ", + "outgoing": "Ելքային", + "outputs": "Ելքեր", + "overwrite_amount": "Գրեք գումարը", + "pairingInvalidEvent": "Սխալ միացում", + "passphrase": "Պարող արտահայտություն (Ոչ պարտադիր)", + "password": "Գաղտնաբառ", + "paste": "Տեղադրել", + "pause_wallet_creation": "Հնարավորություն ստեղծել Haven Դրամապանակ ընթացիկ դադարեցված է", + "payment_id": "Վճարման հերթական համար", + "payment_was_received": "Վճարումը ստացված է", + "pending": " (մշակվում է)", + "percentageOf": "${amount}-ի տոկոս", + "pin_at_top": "${token} պին վերևում", + "pin_is_incorrect": "Պին-կոդը սխալ է", + "pin_number": "Պին-կոդ", + "placeholder_contacts": "Ձեր կոնտակտները այստեղ կցուցադրվեն", + "placeholder_transactions": "Ձեր գործարքները այստեղ կցուցադրվեն", + "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 պատմություն", + "powered_by": "${title} կողմից ապահովված", + "pre_seed_button_text": "Ես հասկանում եմ։ Ցույց տվեք իմ սերմը", + "pre_seed_description": "Հաջորդ էջում դուք կտեսնեք ${words} բառերի շարք։ Սա ձեր յուրահատուկ և գաղտնի սերմն է, որը ձեր դրամապանակը վերականգնելու միակ միջոցն է կորուստի կամ սխալ գործարքի դեպքում։ Դուք պատասխանատու եք այն գրառել և ապահով վայրում պահել Cake Wallet հավելվածից դուրս", + "pre_seed_title": "ԿԱՐԵՎՈՐ", + "prepaid_cards": "Նախավճարային քարտեր", + "prevent_screenshots": "Կանխել էկրանի պատկերները և տեսագրությունը", + "privacy": "Գաղտնիություն", + "privacy_policy": "Գաղտնիության քաղաքականություն", + "privacy_settings": "Գաղտնիության կարգավորումներ", + "private_key": "Գաղտնի բանալի", + "proceed_after_one_minute": "Եթե էկրանը 1 րոպեից ավել չի անցնում, ստուգեք ձեր էլեկտրոնային փոստը", + "proceed_on_device": "Շարունակեք ձեր սարքի վրա", + "proceed_on_device_description": "Խնդրում ենք հետևել ձեր սարքի վրա ցուցադրվող հրահանգներին", + "profile": "Պրոֆիլ", + "provider_error": "${provider} սխալ", + "public_key": "Հանրային բանալի", + "purchase_gift_card": "Նվեր քարտ գնել", + "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": "Ստացողի հասցե", + "reconnect": "Վերակապվել", + "reconnect_alert_text": "Դուք վստահ եք, որ ուզում եք վերակապվել?", + "reconnection": "Վերակապում", + "red_dark_theme": "Կարմիր մութ տեսք", + "red_light_theme": "Կարմիր պայծառ տեսք", + "redeemed": "Փոխանակված", + "refund_address": "Վերադարձի հասցե", + "reject": "Մերժել", + "remaining": "Մնացած", + "remove": "Հեռացնել", + "remove_node": "Հեռացնել հանգույց", + "remove_node_message": "Դուք վստահ եք, որ ուզում եք հեռացնել ընտրված հանգույցը?", + "rename": "Վերանվանել", + "rep_warning": "Ներկայացուցիչ նախազգուշացում", + "rep_warning_sub": "Ձեր ներկայացուցիչը չի հայտնվում լավ վիճակում։ Սեղմեք այստեղ նոր ներկայացուցիչ ընտրելու համար", + "repeat_wallet_password": "Վերականգնել դրամապանակի գաղտնաբառ", + "repeated_password_is_incorrect": "Վերականգնված գաղտնաբառը սխալ է։ Խնդրում ենք վերականգնել դրամապանակի գաղտնաբառը", + "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-ը հետո", + "rescan": "Վերասկանավորել", + "resend_code": "Խնդրում ենք կրկին ուղարկել", + "reset": "Վերասահմանել", + "reset_password": "Վերասահմանել գաղտնաբառը", + "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": "Վերականգնեք Ledger հարդվերային դրամապանակից", + "restore_description_from_keys": "Վերականգնեք ձեր դրամապանակը ձեր գախտնի բանալիների հիման վրա ստեղծված մուտքագրումներից", + "restore_description_from_seed": "Վերականգնեք ձեր դրամապպանակը 25 բառերի կամ 13 բառերի համադրությամբ", + "restore_description_from_seed_keys": "Վերականգնեք ձեր դրամապանակը սերմից/բանալիներից, որը դուք պահպանել եք ապահով վայրում", + "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": "Խնդրում ենք հաստատել, որ դուք պահպանել եք ձեր կրկնօրինակի գաղտնաբառը։ Դուք չեք կարող ներմուծել ձեր կրկնօրինակ ֆայլերը առանց դրա։", + "save_backup_password_alert": "Պահպանել կրկնօրինակի գաղտնաբառը", + "save_to_downloads": "Պահպանել ներբեռնումներում", + "saved_the_trade_id": "Ես պահպանել եմ առևտրի ID-ն", + "scan_one_block": "Սկանավորել մեկ բլոկ", + "scan_qr_code": "Սկանավորել QR կոդ", + "scan_qr_code_to_get_address": "Սկանավորել QR կոդը հասցեն ստանալու համար", + "scan_qr_on_device": "Սկանավորել այս QR կոդը այլ սարքի վրա", + "search": "Որոնել", + "search_add_token": "Որոնել / Ավելացնել token", + "search_category": "Որոնել կատեգորիա", + "search_currency": "Որոնել արժույթ", + "search_language": "Որոնել լեզու", + "second_intro_content": "Ձեր Yat-ը միակ եզակի էմոջի հասցե է, որը փոխարինում է ձեր բոլոր արժույթների համար ձեր բոլոր երկար հեքսադեցիմալ հասցեները", + "second_intro_title": "Մի էմոջի հասցե, որը կառավարում է դրանք բոլորը", + "security_and_backup": "Անվտանգություն և կրկնօրինակ", + "seed_alert_back": "Վերադառնալ", + "seed_alert_content": "Սերմը վերականգնելու ձեր դրամապանակի միակ միջոցն է։ Դուք արդեն գրի եք առել այն?", + "seed_alert_title": "Ուշադրություն", + "seed_alert_yes": "Այո, ես արդեն գրի եմ առել այն", + "seed_choose": "Ընտրել սերմի լեզուն", + "seed_hex_form": "Դրամապանակի սերմ (hex ֆորմատ)", + "seed_key": "Սերմի բանալի", + "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_japanese": "Ճապոներեն", + "seed_language_korean": "Կորեերեն", + "seed_language_next": "Հաջորդ", + "seed_language_portuguese": "Պորտուգալերեն", + "seed_language_russian": "Ռուսերեն", + "seed_language_spanish": "Իսպաներեն", + "seed_phrase_length": "Սերմի արտահայտության երկարություն", + "seed_reminder": "Խնդրում ենք գրի առնել այս տեղեկությունը, եթե դուք կորցնեք կամ ջնջեք ձեր հեռախոսը", + "seed_share": "Կիսվել սերմով", + "seed_title": "Սերմ", + "seedtype": "Սերմի տեսակ", + "seedtype_legacy": "Legacy (25 բառ)", + "seedtype_polyseed": "Polyseed (16 բառ)", + "seedtype_wownero": "Wownero (14 բառ)", + "select_backup_file": "Ընտրել կրկնօրինակ ֆայլ", + "select_buy_provider_notice": "Ընտրեք գնման մատակարարը վերևում։ Դուք կարող եք բաց թողնել այս էկրանը ձեր լռելայն գնման մատակարարը հավելվածի կարգավորումներում սահմանելով", + "select_destination": "Խնդրում ենք ընտրել կրկնօրինակ ֆայլի նպատակակետը", + "select_sell_provider_notice": "Ընտրեք վաճառքի մատակարարը վերևում։ Դուք կարող եք բաց թողնել այս էկրանը ձեր լռելայն վաճառքի մատակարարը հավելվածի կարգավորումներում սահմանելով", + "sell": "Ծախել", + "sell_alert_content": "Մենք ներկայումս պաշտպանում ենք միայն Bitcoin, Ethereum և Litecoin վաճառքը։ Խնդրում ենք ստեղծել կամ միացնել ձեր Bitcoin, Ethereum կամ Litecoin դրամապանակը", + "sell_monero_com_alert_content": "Monero-ի վաճառքը դեռ չի պաշտպանվում", + "send": "Ուղարկել", + "send_address": "${cryptoCurrency} հասցե", + "send_amount": "Քանակ՝", + "send_creating_transaction": "Ստեղծել գործարք", + "send_error_currency": "Արժույթը կարող է պարունակել միայն թվեր", + "send_error_minimum_value": "Քանակի նվազագույն արժեքը 0.01 է", + "send_estimated_fee": "Գնահատված վարձը՝", + "send_fee": "Վարձը՝", + "send_name": "Անվանում", + "send_new": "Նոր", + "send_payment_id": "Վճարման ID (կամավոր)", + "send_priority": "Ներկայումս վարձը սահմանված է ${transactionPriority} առաջնահերթությամբ։ Գործարքի առաջնահերթությունը կարող է կարգավորվել կարգավորումներում", + "send_sending": "Ուղարկվում է...", + "send_success": "Ձեր ${crypto}-ն հաջողությամբ ուղարկվել է", + "send_templates": "Տեղակալներ", + "send_title": "Ուղարկել", + "send_to_this_address": "Ուղարկել ${currency} ${tag} այս հասցեով", + "send_xmr": "Ուղարկել XMR", + "send_your_wallet": "Ձեր դրամապանակ", + "sending": "Ուղարկում", + "sent": "Ուղարկված", + "service_health_disabled": "Ծառայության առողջությունը անջատված է", + "service_health_disabled_message": "Սա ծառայության առողջության էջն է, դուք կարող եք այս էջը միացնել Կարգավորումներ -> Գաղտնիություն", + "settings": "Կարգավորումներ", + "settings_all": "Բոլորը", + "settings_allow_biometrical_authentication": "Թույլատրել կենսաչափական վավերացում", + "settings_can_be_changed_later": "Այս կարգավորումները կարող են փոխվել ավելի ուշ հավելվածի կարգավորումներում", + "settings_change_language": "Փոխել լեզուն", + "settings_change_pin": "Փոխել PIN-ը", + "settings_currency": "Արժույթ", + "settings_current_node": "Ներկայիս հանգույց", + "settings_dark_mode": "Մութ ռեժիմ", + "settings_display_balance": "Ցուցադրել հաշիվը", + "settings_display_on_dashboard_list": "Ցուցադրել վահանակի ցանկում", + "settings_fee_priority": "Վարձի առաջնահերթություն", + "settings_nodes": "Հանգույցներ", + "settings_none": "Ոչինչ", + "settings_only_trades": "Միայն առևտուր", + "settings_only_transactions": "Միայն փոխանցումներ", + "settings_personal": "Անձնական", + "settings_save_recipient_address": "Պահպանել ստացողի հասցե", + "settings_support": "Աջակցություն", + "settings_terms_and_conditions": "Պայմաններ և պայմանավորվածություններ", + "settings_title": "Կարգավորումներ", + "settings_trades": "Առևտուր", + "settings_transactions": "Փոխանցումներ", + "settings_wallets": "Դրամապանակներ", + "setup_2fa": "Հավատարմագրել 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\nԵթե դուք կորցնեք ձեր 2FA հավելվածի կամատեղությունը կամ TOTP բանալիները, դուք կկորցնեք այս դրամապանակի հասանելիությունը։ Դուք կստիպվեք վերականգնել ձեր դրամապանակը մնեմոնիկ սերմերի միջոցով։\n\nCake աջակցությունը չի կարող օգնել ձեզ, եթե դուք կորցնեք ձեր 2FA կամ մնեմոնիկ սերմերը։ Խնդրում ենք կարդալ ուղեցույցը, նախքան Cake 2FA-ն օգտագործելը", + "setup_your_debit_card": "Հավատարմագրել ձեր դեբետային քարտ", + "share": "Կիսվել", + "share_address": "Կիսվել հասցեով", + "show_details": "Ցուցադրել մանրամասներ", + "show_keys": "Ցուցադրել բանալիներ", + "show_market_place": "Ցուցադրել շուկան", + "show_seed": "Ցուցադրել սերմ", + "sign_message": "Կնքել հաղորդագրություն", + "sign_up": "Գրանցվել", + "sign_verify_message": "Կնքել կամ ստուգել հաղորդագրություն", + "sign_verify_message_sub": "Կնքել կամ ստուգել հաղորդագրությունը ձեր գախտնի բանալիով", + "sign_verify_title": "Կնքել / Ստուգել", + "signature": "Կնիք", + "signature_invalid_error": "Կնիքը անվավեր է տրված հաղորդագրության համար", + "signTransaction": "Կնքել Գործարք", + "signup_for_card_accept_terms": "Գրանցվել քարտի համար և ընդունել պայմանները", + "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": "Լուռ Վճարումներ Կարգավորումներ", + "slidable": "Սահելի", + "sort_by": "Դասավորել ըստ", + "spend_key_private": "Վճարման բանալի (գախտնի)", + "spend_key_public": "Վճարման բանալի (հանրային)", + "status": "Կարգավիճակ՝ ", + "string_default": "Լռելայն", + "subaddress_title": "Ենթահասցեների ցանկ", + "subaddresses": "Ենթահասցեներ", + "submit_request": "Ուղարկել հարցում", + "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": "Այլ աջակցության հղումներ", + "sweeping_wallet": "Դրամապանակը մաքրվում է", + "sweeping_wallet_alert": "Սա չի տևի երկար։ Խնդրում ենք չլքել այս էկրանը կամ մաքրված միջոցները կկորչեն։", + "switchToETHWallet": "Խնդրում ենք անցնել Ethereum դրամապանակ և փորձել կրկին", + "switchToEVMCompatibleWallet": "Խնդրում ենք անցնել EVM համատեղելի դրամապանակ և փորձել կրկին (Ethereum, Polygon)", + "symbol": "Նշան", + "sync_all_wallets": "Համաժամեցնել բոլոր դրամապանակները", + "sync_status_attempting_sync": "ՀԱՄԱԺԱՄԵՑՄԱՆ ՓՈՐՁ", + "sync_status_connected": "ՄԻԱՑՎԱԾԷ", + "sync_status_connecting": "ՄԻԱՑՎՈՒՄ Է", + "sync_status_failed_connect": "ՉՄԻԱՑԱՎ", + "sync_status_not_connected": "ՄԻԱՑՎԱԾ ՉԷ", + "sync_status_starting_scan": "ՍԿԱՆԱՎՈՐՈՒՄԸ ՍԿՍՎՈՒՄ Է", + "sync_status_starting_sync": "ՀԱՄԱԺԱՄԵՑՈՒՄԸ ՍԿՍՎՈՒՄ Է", + "sync_status_syncronized": "ՀԱՄԱԺԱՄԵՑՎԱԾԷ", + "sync_status_syncronizing": "ՀԱՄԱԺԱՄԵՑՎՈՒՄ Է", + "sync_status_timed_out": "Ժամանակը սպառվեց", + "sync_status_unsupported": "ՉԱՋԱԿՑՎՈՂ ՀԱՆԳՈՒՅՑ,", + "syncing_wallet_alert_content": "Ձեր հաշիվը և գործարքների ցանկը կարող են լինել անավարտ մինչև վերին մասում գրված լինի “ՀԱՂՈՒՄ”։ Սեղմեք/դիպեք ուսումնասիրելու համար։", + "syncing_wallet_alert_title": "Ձեր դրամապանակը համաժամացվում է", + "template": "Տարբերակ", + "template_name": "Տարբերակի անուն", + "testnet_coins_no_value": "Testnet արժույթները չունեն արժեք", + "third_intro_content": "Yats-ը ապրում է Cake Wallet-ի դրսում ևս: Երկրի ցանկացած դրամապանակի հասցե կարող է փոխարինվել Yat-ով!", + "third_intro_title": "Yat-ը լավ է համագործակցում ուրիշների հետ", + "thorchain_contract_address_not_supported": "THORChain-ը չի աջակցում պայմանագրի հասցե ուղարկելուն", + "thorchain_taproot_address_not_supported": "ThorChain մատակարարը չի աջակցում Taproot հասցեները: Խնդրում ենք փոխել հասցեն կամ ընտրել այլ մատակարար:", + "time": "${minutes}ր ${seconds}վ", + "tip": "Թեյավճար", + "today": "Այսօր", + "token_contract_address": "Token-ի պայմանագրի հասցե", + "token_decimal": "Token-ի տասանիշ", + "token_name": "Token-ի անուն, օրինակ՝ Tether", + "token_symbol": "Token-ի նշան, օրինակ՝ USDT", + "tokenID": "ID", + "tor_connection": "Tor կապ", + "tor_only": "Միայն Tor", + "total": "Ընդհանուր", + "total_saving": "Ընդհանուր խնայողություն", + "totp_2fa_failure": "Սխալ կոդ: Խնդրում ենք փորձել այլ կոդ կամ ստեղծել նոր գաղտնի բանալի: Օգտագործեք համատեղելի 2FA ծրագիր, որը աջակցում է 8-նիշերի կոդեր և SHA512:", + "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": "Բեռնում", + "trade_details_id": "ID", + "trade_details_pair": "Զույգ", + "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": "BTC ուղարկված է", + "trade_state_complete": "Ավարտված է", + "trade_state_confirming": "Հաստատվում է", + "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": "Վճարված չէ", + "trades": "Գործարքներ", + "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_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": "Փոխանցումներ ամսաթվով", + "trongrid_history": "TronGrid պատմություն", + "trusted": "Վստահելի", + "tx_commit_exception_no_dust_on_change": "Փոխանցումը մերժվել է այս գումարով: Այս արժույթներով կարող եք ուղարկել ${min} առանց փոփոխության կամ ${max} որը վերադարձնում է փոփոխությունը", + "tx_commit_failed": "Փոխանցումը ձախողվել է: Խնդրում ենք դիմել աջակցությանը", + "tx_invalid_input": "Դուք օգտագործում եք սխալ մուտքային տիպ այս տեսակի վճարման համար", + "tx_no_dust_exception": "Փոխանցումը մերժվել է շատ փոքր գումարով: Խնդրում ենք փորձել ավելացնել գումարը", + "tx_not_enough_inputs_exception": "Չկան բավարար մուտքեր: Խնդրում ենք ընտրել ավելին Coin Control֊ում", + "tx_rejected_bip68_final": "Փոխանցումը մերժվել է չհաստատված մուտքերով և ձախողվել է փոխարինել վճարման համար", + "tx_rejected_dust_change": "Փոխանցումը մերժվել է ցածր փոփոխության գումարով (փոշի): Փորձեք ուղարկել ԲՈԼՈՐ կամ նվազեցնել գումարը", + "tx_rejected_dust_output": "Փոխանցումը մերժվել է ցածր ելքային գումարով (փոշի): Խնդրում ենք ավելացնել գումարը", + "tx_rejected_dust_output_send_all": "Փոխանցումը մերժվել է ցածր ելքային գումարով (փոշի): Խնդրում ենք ստուգել արժույթների հաշիվը մուտքային վերահսկողության տակ", + "tx_rejected_vout_negative": "Բավարար մնացորդ չկա այս փոխանցման վճարների համար։ Խնդրում ենք ստուգել արժույթների մնացորդը Coin Control-ում։", + "tx_wrong_balance_exception": "Դուք չունեք բավարար ${currency} այս գումարը ուղարկելու համար։", + "tx_wrong_balance_with_amount_exception": "Դուք չունեք բավարար ${currency} ${amount} գումարը ուղարկելու համար։", + "tx_zero_fee_exception": "Չի կարող ուղարկվել Փոխանցումը առանց վճարի։ Փորձեք բարձրացնել գինը կամ ստուգել ձեր կապը վերջին գնահատականների համար։", + "unavailable_balance": "Անհասանելի մնացորդ", + "unavailable_balance_description": "Անհասանելի մնացորդ: Այս ընդհանուր գումարը ներառում է այն միջոցները, որոնք արգելափակված են ընթացիկ գործարքներում և նրանք, որոնք դուք ակտիվորեն սառեցրել եք արժույթների վերահսկման կարգավորումների մեջ։ Արգելափակված մնացորդները կդառնան հասանելի, երբ համապատասխան գործարքները կավարտվեն, մինչդեռ սառեցված մնացորդները կմնան անհասանելի մինչև դուք որոշեք դրանք ապասառեցնել։", + "unconfirmed": "Չհաստատված մնացորդ", + "understand": "Ես հասկանում եմ", + "unlock": "Բացել", + "unmatched_currencies": "Ձեր ընթացիկ դրամապանակի արժույթը չի համապատասխանում սկանավորված QR կոդի արժույթին", + "unspent_change": "Մնացորդ", + "unspent_coins_details_title": "Չծախսված արժույթների մանրամասները", + "unspent_coins_title": "Չծախսված արժույթներ", + "unsupported_asset": "Մենք չենք աջակցում այս գործողությունը այս ակտիվի համար։ Խնդրում ենք ստեղծել կամ անցնել աջակցվող ակտիվի տեսակին համապատասխան դրամապանակի։", + "uptime": "Աշխատանքային ժամանակ", + "upto": "մինչև ${value}", + "usb": "USB", + "use": "Փոխեք ", + "use_card_info_three": "Օգտագործեք թվային քարտը առցանց կամ անշփման վճարման մեթոդներով։", + "use_card_info_two": "Միջոցները փոխարկվում են ԱՄՆ դոլար երբ դրանք պահվում են կանխավճարային հաշվեկշռում, ոչ թե թվային արժույթներում։", + "use_ssl": "Օգտագործել SSL", + "use_suggested": "Օգտագործել առաջարկվածը", + "use_testnet": "Օգտագործել Testnet", + "value": "Արժեք", + "value_type": "Արժեքի տեսակ", + "variable_pair_not_supported": "Այս փոփոխականի զույգը չի աջակցվում ընտրված բորսաների հետ", + "verification": "Ստուգում", + "verify_message": "Ստուգել հաղորդագրությունը", + "verify_with_2fa": "Ստուգեք Cake 2FA-ով", + "version": "Տարբերակ ${currentVersion}", + "view_all": "Դիտել բոլորը", + "view_in_block_explorer": "Դիտել Block Explorer-ում", + "view_key_private": "Դիտել բանալին (գախտնի)", + "view_key_public": "Դիտել բանալին (հանրային)", + "view_transaction_on": "Դիտել Գործարքը ", + "voting_weight": "Քվեարկության Քաշ", + "waitFewSecondForTxUpdate": "Խնդրում ենք սպասել մի քանի վայրկյան, որպեսզի գործարքը արտացոլվի գործարքների պատմության մեջ", + "wallet_keys": "Դրամապանակի սերմ/բանալիներ", + "wallet_list_create_new_wallet": "Ստեղծել Նոր Դրամապանակ", + "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": "Monero Դրամապանակ", + "wallet_list_wallet_name": "Դրամապանակի անունը", + "wallet_menu": "Մենյու", + "wallet_name": "Դրամապանակի անունը", + "wallet_name_exists": "Այդ անունով դրամապանակ արդեն կա։ Խնդրում ենք ընտրել այլ անուն կամ վերանվանել մյուս դրամապանակը։", + "wallet_password_is_empty": "Դրամապանակի գաղտնաբառը դատարկ է։ Դրամապանակի գաղտնաբառը չպետք է դատարկ լինի", + "wallet_recovery_height": "Վերականգնման բարձրություն", + "wallet_restoration_store_incorrect_seed_length": "Սերմի երկարությունը սխալ է", + "wallet_seed": "Դրամապանակի սերմ", + "wallet_seed_legacy": "Legacy դրամապանակի սերմ", + "wallet_store_monero_wallet": "Monero Դրամապանակ", + "walletConnect": "WalletConnect", + "wallets": "Դրամապանակներ", + "warning": "Զգուշացում", + "welcome": "Բարի գալուստ", + "welcome_to_cakepay": "Բարի գալուստ Cake Pay!", + "what_is_silent_payments": "Ի՞նչ է Լուռ Վճարումները:", + "widgets_address": "Հասցե", + "widgets_or": "կամ", + "widgets_restore_from_blockheight": "Վերականգնել բլոկի բարձրությունից", + "widgets_restore_from_date": "Վերականգնել ամսաթվից", + "widgets_seed": "Սերմ", + "wouoldLikeToConnect": "ցանկանում է միանալ", + "write_down_backup_password": "Խնդրում ենք գրանցել ձեր կրկնօրինակի գաղտնաբառը, որը օգտագործվում է կրկնօրինակ ֆայլերի ներմուծման համար։", + "xlm_extra_info": "Խնդրում ենք չմոռանալ նշել Memo ID-ն երբ ուղարկում եք XLM գործարք փոխանակման համար", + "xmr_available_balance": "Հասանելի մնացորդ", + "xmr_full_balance": "Լրիվ մնացորդ", + "xmr_hidden": "Թաքնված", + "xmr_to_error": "XMR.TO սխալ", + "xmr_to_error_description": "Սխալ գումար։ Առավելագույն սահմանը 8 թվանշան է կետից հետո", + "xrp_extra_info": "Խնդրում ենք չմոռանալ նշել Destination Tag-ը երբ ուղարկում եք XRP գործարք փոխանակման համար", + "yat": "Yat", + "yat_address": "Yat Հասցե", + "yat_alert_content": "Cake Wallet-ի օգտատերերը այժմ կարող են ուղարկել և ստանալ իրենց սիրած արժույթները emoji-ների հիման վրա օգտագործվող անունով։", + "yat_alert_title": "Ուղարկել և ստանալ կրիպտո ավելի հեշտությամբ Yat-ի միջոցով", + "yat_error": "Yat սխալ", + "yat_error_content": "Այս Yat-ի հետ կապված հասցեներ չկան։ Փորձեք այլ Yat", + "yat_popup_content": "Այժմ դուք կարող եք ուղարկել և ստանալ կրիպտո Cake Wallet-ում ձեր Yat-ով՝ կարճ, emoji-ների վրա հիմնված օգտագործողի անունով։ Կառավարեք Yat-երը ցանկացած ժամանակ կարգավորումների էկրանին", + "yat_popup_title": "Ձեր դրամապանակի հասցեն կարող է emoji-ացվել։", + "yesterday": "Երեկ", + "you_now_have_debit_card": "Դուք այժմ ունեք դեբետային քարտ", + "you_pay": "Դուք վճարում եք", + "you_will_get": "Ստացեք", + "you_will_send": "Փոխանակեք", + "yy": "ՏՏ" +} \ No newline at end of file From 47ca649060a48f98469fcaf93de6196c8104b2d6 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 22 Aug 2024 03:55:23 +0300 Subject: [PATCH 054/142] update translations --- README.md | 4 +++- tool/utils/translation/translation_constants.dart | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e507bfcd..1c28f92a2 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,9 @@ The only parts to be translated, if needed, are the values m and s after the var 4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language. -5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 digit localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop. +5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop. + +6. Add the new language code to `tool/utils/translation/translation_constants.dart` ## Add a new fiat currency diff --git a/tool/utils/translation/translation_constants.dart b/tool/utils/translation/translation_constants.dart index 3a472d8c4..3d1afe142 100644 --- a/tool/utils/translation/translation_constants.dart +++ b/tool/utils/translation/translation_constants.dart @@ -1,6 +1,6 @@ const defaultLang = "en"; const langs = [ - "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "id", "it", + "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "hy", "id", "it", "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tl", "tr", "uk", "ur", "yo", "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified) ]; From 9c3f0b329bba802787123d7c9ac5c745bf56121f Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 22 Aug 2024 15:34:31 +0300 Subject: [PATCH 055/142] minor fix --- lib/entities/language_service.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/entities/language_service.dart b/lib/entities/language_service.dart index bb60c5cce..260c8a27f 100644 --- a/lib/entities/language_service.dart +++ b/lib/entities/language_service.dart @@ -29,7 +29,7 @@ class LanguageService { 'id': 'Bahasa Indonesia (Indonesian)', 'yo': 'Yorùbá (Yoruba)', 'ha': 'Hausa Najeriya (Nigeria)', - 'tl': 'Filipino (Tagalog)' + 'tl': 'Filipino (Tagalog)', 'hy': 'Հայերեն (Armenian)', }; @@ -59,7 +59,7 @@ class LanguageService { 'id': 'idn', 'yo': 'nga', 'ha': 'hau', - 'tl': 'phl' + 'tl': 'phl', 'hy': 'arm' }; From 4c2d0613635a1457c2a63ba788f15dae170fcae9 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 23 Aug 2024 16:19:42 +0300 Subject: [PATCH 056/142] rbf fixes issues sum utxo and fee calculation (#1625) * total out amount issue * fix empty inputs and outputs addresses for new tx * fix sum value of utxo not spending * Update configure.dart * Update electrum_wallet.dart * receiving address * review fixes --- cw_bitcoin/lib/electrum_transaction_info.dart | 2 +- cw_bitcoin/lib/electrum_wallet.dart | 133 +++++++++++------- lib/bitcoin/cw_bitcoin.dart | 5 +- lib/view_model/send/send_view_model.dart | 44 ++++-- .../transaction_details_view_model.dart | 23 +-- tool/configure.dart | 3 +- 6 files changed, 132 insertions(+), 78 deletions(-) diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index ea4a3de33..ee3daa0e0 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -235,6 +235,6 @@ class ElectrumTransactionInfo extends TransactionInfo { } String toString() { - return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)'; + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)'; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index da91a29e4..2a57c8d5c 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -132,6 +132,7 @@ abstract class ElectrumWalletBase final String? _mnemonic; Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); + Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1)); final EncryptionFileUtils encryptionFileUtils; @@ -1363,26 +1364,15 @@ abstract class ElectrumWalletBase } } - Future canReplaceByFee(String hash) async { - final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash); - - final String? transactionHex; - int confirmations = 0; - - if (verboseTransaction.isEmpty) { - transactionHex = await electrumClient.getTransactionHex(hash: hash); - } else { - confirmations = verboseTransaction['confirmations'] as int? ?? 0; - transactionHex = verboseTransaction['hex'] as String?; - } - - if (confirmations > 0) return false; - - if (transactionHex == null || transactionHex.isEmpty) { + Future canReplaceByFee(ElectrumTransactionInfo tx) async { + try { + final bundle = await getTransactionExpanded(hash: tx.txHash); + _updateInputsAndOutputs(tx, bundle); + if (bundle.confirmations > 0) return false; + return bundle.originalTransaction.canReplaceByFee; + } catch (e) { return false; } - - return BtcTransaction.fromRaw(transactionHex).canReplaceByFee; } Future isChangeSufficientForFee(String txId, int newFee) async { @@ -1458,47 +1448,59 @@ abstract class ElectrumWalletBase ); } - int totalOutAmount = bundle.originalTransaction.outputs - .fold(0, (previousValue, element) => previousValue + element.amount.toInt()); - - var currentFee = allInputsAmount - totalOutAmount; - int remainingFee = newFee - currentFee; - + // Create a list of available outputs final outputs = []; - - // Add outputs and deduct the fees from it - for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) { - final out = bundle.originalTransaction.outputs[i]; + for (final out in bundle.originalTransaction.outputs) { final address = addressFromOutputScript(out.scriptPubKey, network); final btcAddress = addressTypeFromStr(address, network); + outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt()))); + } - int newAmount; - if (out.amount.toInt() >= remainingFee) { - newAmount = out.amount.toInt() - remainingFee; - remainingFee = 0; + // Calculate the total amount and fees + int totalOutAmount = + outputs.fold(0, (previousValue, output) => previousValue + output.value.toInt()); + int currentFee = allInputsAmount - totalOutAmount; + int remainingFee = newFee - currentFee; - // if new amount of output is less than dust amount, then don't add this output as well - if (newAmount <= _dustAmount) { - continue; + if (remainingFee <= 0) { + throw Exception("New fee must be higher than the current fee."); + } + + // Deduct Remaining Fee from Main Outputs + if (remainingFee > 0) { + for (int i = outputs.length - 1; i >= 0; i--) { + int outputAmount = outputs[i].value.toInt(); + + if (outputAmount > _dustAmount) { + int deduction = (outputAmount - _dustAmount >= remainingFee) + ? remainingFee + : outputAmount - _dustAmount; + outputs[i] = BitcoinOutput( + address: outputs[i].address, value: BigInt.from(outputAmount - deduction)); + remainingFee -= deduction; + + if (remainingFee <= 0) break; } - } else { - remainingFee -= out.amount.toInt(); - continue; } - - outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount))); } + // Final check if the remaining fee couldn't be deducted + if (remainingFee > 0) { + throw Exception("Not enough funds to cover the fee."); + } + + // Identify all change outputs final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); + final List changeOutputs = outputs + .where((output) => changeAddresses + .any((element) => element.address == output.address.toAddress(network))) + .toList(); - // look for a change address in the outputs - final changeOutput = outputs.firstWhereOrNull((output) => - changeAddresses.any((element) => element.address == output.address.toAddress(network))); + int totalChangeAmount = + changeOutputs.fold(0, (sum, output) => sum + output.value.toInt()); - // deduct the change amount from the output amount - if (changeOutput != null) { - totalOutAmount -= changeOutput.value.toInt(); - } + // The final amount that the receiver will receive + int sendingAmount = allInputsAmount - newFee - totalChangeAmount; final txb = BitcoinTransactionBuilder( utxos: utxos, @@ -1527,10 +1529,10 @@ abstract class ElectrumWalletBase transaction, type, electrumClient: electrumClient, - amount: totalOutAmount, + amount: sendingAmount, fee: newFee, network: network, - hasChange: changeOutput != null, + hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), )..addListener((transaction) async { transactionHistory.addOne(transaction); @@ -2026,6 +2028,39 @@ abstract class ElectrumWalletBase }); } } + + void _updateInputsAndOutputs(ElectrumTransactionInfo tx, ElectrumTransactionBundle bundle) { + tx.inputAddresses = tx.inputAddresses?.where((address) => address.isNotEmpty).toList(); + + if (tx.inputAddresses == null || + tx.inputAddresses!.isEmpty || + tx.outputAddresses == null || + tx.outputAddresses!.isEmpty) { + List inputAddresses = []; + List outputAddresses = []; + + for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) { + final input = bundle.originalTransaction.inputs[i]; + final inputTransaction = bundle.ins[i]; + final vout = input.txIndex; + final outTransaction = inputTransaction.outputs[vout]; + final address = addressFromOutputScript(outTransaction.scriptPubKey, network); + + if (address.isNotEmpty) inputAddresses.add(address); + } + + for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) { + final out = bundle.originalTransaction.outputs[i]; + final address = addressFromOutputScript(out.scriptPubKey, network); + + if (address.isNotEmpty) outputAddresses.add(address); + } + tx.inputAddresses = inputAddresses; + tx.outputAddresses = outputAddresses; + + transactionHistory.addOne(tx); + } + } } class ScanNode { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 5a71e3549..d979f9281 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -398,9 +398,10 @@ class CWBitcoin extends Bitcoin { } @override - Future canReplaceByFee(Object wallet, String transactionHash) async { + Future canReplaceByFee(Object wallet, Object transactionInfo) async { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.canReplaceByFee(transactionHash); + final tx = transactionInfo as ElectrumTransactionInfo; + return bitcoinWallet.canReplaceByFee(tx); } @override diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 863c83957..22c083455 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -18,6 +18,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/exceptions.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; @@ -392,25 +393,38 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } @action - Future replaceByFee(String txId, String newFee) async { + Future replaceByFee(TransactionInfo tx, String newFee) async { state = IsExecutingState(); - final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, txId, newFee); + try { + final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, tx.id, newFee); - if (!isSufficient) { - state = AwaitingConfirmationState( - title: S.current.confirm_fee_deduction, - message: S.current.confirm_fee_deduction_content, - onConfirm: () async { - pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee); - state = ExecutedSuccessfullyState(); - }, - onCancel: () { - state = FailureState('Insufficient change for fee'); - }); - } else { - pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee); + if (!isSufficient) { + state = AwaitingConfirmationState( + title: S.current.confirm_fee_deduction, + message: S.current.confirm_fee_deduction_content, + onConfirm: () async => await _executeReplaceByFee(tx, newFee), + onCancel: () => state = FailureState('Insufficient change for fee')); + } else { + await _executeReplaceByFee(tx, newFee); + } + } catch (e) { + state = FailureState(e.toString()); + } + } + + Future _executeReplaceByFee(TransactionInfo tx, String newFee) async { + + + clearOutputs(); + final output = outputs.first; + output.address = tx.outputAddresses?.first ?? ''; + + try { + pendingTransaction = await bitcoin!.replaceByFee(wallet, tx.id, newFee); state = ExecutedSuccessfullyState(); + } catch (e) { + state = FailureState(e.toString()); } } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 18715e508..aa63ce860 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -52,7 +52,7 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.bitcoin: _addElectrumListItems(tx, dateFormat); _addBumpFeesListItems(tx); - _checkForRBF(); + _checkForRBF(tx); break; case WalletType.litecoin: case WalletType.bitcoinCash: @@ -349,12 +349,15 @@ abstract class TransactionDetailsViewModelBase with Store { void _addBumpFeesListItems(TransactionInfo tx) { transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium(); + final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true) + ? 1 + : transactionInfo.inputAddresses!.length; + final outputsCount = (transactionInfo.outputAddresses?.isEmpty ?? true) + ? 1 + : transactionInfo.outputAddresses!.length; newFee = bitcoin!.getFeeAmountForPriority( - wallet, - bitcoin!.getBitcoinTransactionPriorityMedium(), - transactionInfo.inputAddresses?.length ?? 1, - transactionInfo.outputAddresses?.length ?? 1); + wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount); RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0')); @@ -383,12 +386,12 @@ abstract class TransactionDetailsViewModelBase with Store { return setNewFee(value: sliderValue, priority: transactionPriority!); })); - if (transactionInfo.inputAddresses != null) { + if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) { RBFListItems.add(StandardExpandableListItem( title: S.current.inputs, expandableItems: transactionInfo.inputAddresses!)); } - if (transactionInfo.outputAddresses != null) { + if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) { RBFListItems.add(StandardExpandableListItem( title: S.current.outputs, expandableItems: transactionInfo.outputAddresses!)); } @@ -416,10 +419,10 @@ abstract class TransactionDetailsViewModelBase with Store { } @action - Future _checkForRBF() async { + Future _checkForRBF(TransactionInfo tx) async { if (wallet.type == WalletType.bitcoin && transactionInfo.direction == TransactionDirection.outgoing) { - if (await bitcoin!.canReplaceByFee(wallet, transactionInfo.id)) { + if (await bitcoin!.canReplaceByFee(wallet, tx)) { _canReplaceByFee = true; } } @@ -441,7 +444,7 @@ abstract class TransactionDetailsViewModelBase with Store { return bitcoin!.formatterBitcoinAmountToString(amount: newFee); } - void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo.id, newFee); + void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee,); @computed String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled diff --git a/tool/configure.dart b/tool/configure.dart index a0104c34e..c9b6bbdda 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -79,6 +79,7 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/output_info.dart'; @@ -204,7 +205,7 @@ abstract class Bitcoin { bool isTestnet(Object wallet); Future replaceByFee(Object wallet, String transactionHash, String fee); - Future canReplaceByFee(Object wallet, String transactionHash); + Future canReplaceByFee(Object wallet, Object tx); Future isChangeSufficientForFee(Object wallet, String txId, String newFee); int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size}); int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, From 8524e238b0b726ed5b71ebc38ae3b8d0477b2f66 Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 24 Aug 2024 03:42:50 +0200 Subject: [PATCH 057/142] make analyzer less red (#1628) * remove cw_*.dart files from analyzer * Run gen_framework.sh on iOS --- analysis_options.yaml | 13 ++++++++++++- tool/download_moneroc_prebuilds.dart | 10 ++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 396904041..2063b5a8c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,7 +10,18 @@ analyzer: lib/generated/*.dart, cw_monero/ios/External/**, cw_shared_external/**, - shared_external/**] + shared_external/**, + lib/bitcoin/cw_bitcoin.dart, + lib/bitcoin_cash/cw_bitcoin_cash.dart, + lib/ethereum/cw_ethereum.dart, + lib/haven/cw_haven.dart, + lib/monero/cw_monero.dart, + lib/nano/cw_nano.dart, + lib/polygon/cw_polygon.dart, + lib/solana/cw_solana.dart, + lib/tron/cw_tron.dart, + lib/wownero/cw_wownero.dart, + ] language: strict-casts: true strict-raw-types: true diff --git a/tool/download_moneroc_prebuilds.dart b/tool/download_moneroc_prebuilds.dart index 58e8d4b03..9df0b4e7b 100644 --- a/tool/download_moneroc_prebuilds.dart +++ b/tool/download_moneroc_prebuilds.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:archive/archive_io.dart'; @@ -47,4 +49,12 @@ Future main() async { outputStream.writeBytes(archive); } } + if (Platform.isMacOS) { + print("Generating ios framework"); + final result = Process.runSync("bash", [ + "-c", + "cd scripts/ios && ./gen_framework.sh && cd ../.." + ]); + print((result.stdout+result.stderr).toString().trim()); + } } \ No newline at end of file From 40f85d215b2a4064c3586ededd57dba67336af4e Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 24 Aug 2024 16:30:01 +0300 Subject: [PATCH 058/142] Rbf fixes issues op return data plus ThorChain (#1648) * total out amount issue * fix empty inputs and outputs addresses for new tx * fix sum value of utxo not spending * Update configure.dart * Update electrum_wallet.dart * receiving address * review fixes * add op_return data * fix rbf transaction with a memo * add memo check for ThorChain trade * code enhancement [skip ci] * code enhancement [skip ci] * directly use fromElectrumBundle function to update transaction info --------- Co-authored-by: Omar Hatem --- cw_bitcoin/lib/electrum_transaction_info.dart | 23 ++++++++- cw_bitcoin/lib/electrum_wallet.dart | 48 +++++++++++-------- .../exchange/exchange_view_model.dart | 7 +++ .../transaction_details_view_model.dart | 11 ++++- 4 files changed, 65 insertions(+), 24 deletions(-) diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index ee3daa0e0..ebd90ff2b 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -7,10 +9,12 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/format_amount.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:hex/hex.dart'; class ElectrumTransactionBundle { ElectrumTransactionBundle(this.originalTransaction, {required this.ins, required this.confirmations, this.time}); + final BtcTransaction originalTransaction; final List ins; final int? time; @@ -125,7 +129,24 @@ class ElectrumTransactionInfo extends TransactionInfo { for (final out in bundle.originalTransaction.outputs) { totalOutAmount += out.amount.toInt(); final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network)); - outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network)); + final address = addressFromOutputScript(out.scriptPubKey, network); + + if (address.isNotEmpty) outputAddresses.add(address); + + // Check if the script contains OP_RETURN + final script = out.scriptPubKey.script; + if (script.contains('OP_RETURN')) { + final index = script.indexOf('OP_RETURN'); + if (index + 1 <= script.length) { + try { + final opReturnData = script[index + 1].toString(); + final decodedString = utf8.decode(HEX.decode(opReturnData)); + outputAddresses.add('OP_RETURN:$decodedString'); + } catch (_) { + outputAddresses.add('OP_RETURN:'); + } + } + } if (addressExists) { receivedAmounts.add(out.amount.toInt()); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 2a57c8d5c..b763b175b 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -43,6 +43,7 @@ import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; +import 'package:hex/hex.dart'; part 'electrum_wallet.g.dart'; @@ -1413,6 +1414,7 @@ abstract class ElectrumWalletBase List privateKeys = []; var allInputsAmount = 0; + String? memo; // Add inputs for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { @@ -1451,6 +1453,22 @@ abstract class ElectrumWalletBase // Create a list of available outputs final outputs = []; for (final out in bundle.originalTransaction.outputs) { + + // Check if the script contains OP_RETURN + final script = out.scriptPubKey.script; + if (script.contains('OP_RETURN') && memo == null) { + final index = script.indexOf('OP_RETURN'); + if (index + 1 <= script.length) { + try { + final opReturnData = script[index + 1].toString(); + memo = utf8.decode(HEX.decode(opReturnData)); + continue; + } catch (_) { + throw Exception('Cannot decode OP_RETURN data'); + } + } + } + final address = addressFromOutputScript(out.scriptPubKey, network); final btcAddress = addressTypeFromStr(address, network); outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt()))); @@ -1507,6 +1525,8 @@ abstract class ElectrumWalletBase outputs: outputs, fee: BigInt.from(newFee), network: network, + memo: memo, + outputOrdering: BitcoinOrdering.none, enableRBF: true, ); @@ -2036,27 +2056,13 @@ abstract class ElectrumWalletBase tx.inputAddresses!.isEmpty || tx.outputAddresses == null || tx.outputAddresses!.isEmpty) { - List inputAddresses = []; - List outputAddresses = []; - - for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) { - final input = bundle.originalTransaction.inputs[i]; - final inputTransaction = bundle.ins[i]; - final vout = input.txIndex; - final outTransaction = inputTransaction.outputs[vout]; - final address = addressFromOutputScript(outTransaction.scriptPubKey, network); - - if (address.isNotEmpty) inputAddresses.add(address); - } - - for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) { - final out = bundle.originalTransaction.outputs[i]; - final address = addressFromOutputScript(out.scriptPubKey, network); - - if (address.isNotEmpty) outputAddresses.add(address); - } - tx.inputAddresses = inputAddresses; - tx.outputAddresses = outputAddresses; + tx = ElectrumTransactionInfo.fromElectrumBundle( + bundle, + walletInfo.type, + network, + addresses: addressesSet, + height: tx.height, + ); transactionHistory.addOne(tx); } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index f2ea8eeb4..2bbe9954e 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -845,6 +845,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ); } + if ((trade.memo == null || trade.memo!.isEmpty)) { + return CreateTradeResult( + result: false, + errorMessage: 'Memo is required for Thorchain trade', + ); + } + final currenciesToCheckPattern = RegExp('0x[0-9a-zA-Z]'); // Perform checks for payOutAddress diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index aa63ce860..e4f9c3786 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -392,8 +392,15 @@ abstract class TransactionDetailsViewModelBase with Store { } if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) { - RBFListItems.add(StandardExpandableListItem( - title: S.current.outputs, expandableItems: transactionInfo.outputAddresses!)); + final outputAddresses = transactionInfo.outputAddresses!.map((element) { + if (element.contains('OP_RETURN:') && element.length > 40) { + return element.substring(0, 40) + '...'; + } + return element; + }).toList(); + + RBFListItems.add( + StandardExpandableListItem(title: S.current.outputs, expandableItems: outputAddresses)); } } From ce21098e9824edf4f8d828f127e4d1aaeefee213 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 26 Aug 2024 19:06:54 +0200 Subject: [PATCH 059/142] Cw 488 seed offset (#1631) * CW-488 minor code cleanup * Add Derivation Path selector for BTC and LTC * CW-488 Initial Passphrase Impl * CW-488 Final Passphrase Impl * Quick Fix of language Service * CW-488 Implement PR Suggestions * CW-488 Implement PR Suggestions * CW-488 Implement Passphrase for Bitcoin Cash * CW-488 Implement Passphrase for Bitcoin Cash * CW-488 Implement Passphrase for Bitcoin Cash * remove monero and wownero support for passphrase until merged [skip ci] * CW-488 Apply requested change * CW-488 Add Passphrase to QR * CW-488 Fix Seed generation * CW-488 Implement Electrum Passphrases * CW-488 Add Seed Length Selector to BIP39 Seeds * CW-488 Minor fix [skip ci] --------- Co-authored-by: Omar Hatem --- cw_bitcoin/lib/bitcoin_mnemonic.dart | 18 +- .../lib/bitcoin_mnemonics_bip39.dart | 3 +- cw_bitcoin/lib/bitcoin_wallet.dart | 4 +- .../bitcoin_wallet_creation_credentials.dart | 16 +- cw_bitcoin/lib/bitcoin_wallet_service.dart | 16 +- cw_bitcoin/lib/electrum_derivations.dart | 3 +- cw_bitcoin/lib/electrum_wallet.dart | 2 + cw_bitcoin/lib/litecoin_wallet.dart | 34 +++- cw_bitcoin/lib/litecoin_wallet_service.dart | 16 +- .../lib/src/bitcoin_cash_base.dart | 1 - .../lib/src/bitcoin_cash_wallet.dart | 14 +- ...coin_cash_wallet_creation_credentials.dart | 18 +- .../lib/src/bitcoin_cash_wallet_service.dart | 30 ++-- cw_core/lib/wallet_base.dart | 2 + lib/bitcoin/cw_bitcoin.dart | 11 +- lib/bitcoin_cash/cw_bitcoin_cash.dart | 8 +- lib/core/wallet_creation_service.dart | 6 +- lib/di.dart | 34 ++-- lib/entities/language_service.dart | 2 +- lib/entities/preferences_key.dart | 1 + lib/entities/seed_type.dart | 46 +++-- lib/router.dart | 97 +++++------ .../dashboard/widgets/verify_form.dart | 16 -- .../advanced_privacy_settings_page.dart | 140 ++++++++++----- .../screens/new_wallet/new_wallet_page.dart | 61 ++++--- .../wallet_restore_from_seed_form.dart | 164 ++++++++++-------- .../screens/restore/wallet_restore_page.dart | 13 +- lib/src/widgets/seed_language_picker.dart | 34 ++-- lib/src/widgets/seed_language_selector.dart | 4 +- lib/store/seed_settings_store.dart | 11 ++ lib/store/settings_store.dart | 77 +++++--- .../advanced_privacy_settings_view_model.dart | 18 +- .../restore/restore_from_qr_vm.dart | 5 +- lib/view_model/seed_settings_view_model.dart | 34 ++++ lib/view_model/seed_type_view_model.dart | 19 -- lib/view_model/wallet_creation_vm.dart | 42 +++-- .../wallet_hardware_restore_view_model.dart | 12 +- lib/view_model/wallet_keys_view_model.dart | 3 +- lib/view_model/wallet_new_vm.dart | 66 ++++--- lib/view_model/wallet_restore_view_model.dart | 40 +++-- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 7 +- res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + tool/configure.dart | 6 +- 67 files changed, 741 insertions(+), 438 deletions(-) rename cw_bitcoin_cash/lib/src/mnemonic.dart => cw_bitcoin/lib/bitcoin_mnemonics_bip39.dart (65%) create mode 100644 lib/store/seed_settings_store.dart create mode 100644 lib/view_model/seed_settings_view_model.dart delete mode 100644 lib/view_model/seed_type_view_model.dart diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart index 905aece28..0749627e9 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; + import 'package:crypto/crypto.dart'; import 'package:cryptography/cryptography.dart' as cryptography; import 'package:cw_core/sec_random_native.dart'; @@ -59,11 +60,7 @@ void maskBytes(Uint8List bytes, int bits) { } } -String bufferToBin(Uint8List data) { - final q1 = data.map((e) => e.toRadixString(2).padLeft(8, '0')); - final q2 = q1.join(''); - return q2; -} +String bufferToBin(Uint8List data) => data.map((e) => e.toRadixString(2).padLeft(8, '0')).join(''); String encode(Uint8List data) { final dataBitLen = data.length * 8; @@ -112,17 +109,18 @@ Future checkIfMnemonicIsElectrum2(String mnemonic) async { Future getMnemonicHash(String mnemonic) async { final hmacSha512 = Hmac(sha512, utf8.encode('Seed version')); final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic))); - final hx = digest.toString(); - return hx; + return digest.toString(); } -Future mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async { +Future mnemonicToSeedBytes(String mnemonic, + {String prefix = segwit, String passphrase = ''}) async { final pbkdf2 = cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512); final text = normalizeText(mnemonic); - // pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce) + final passphraseBytes = utf8.encode(normalizeText(passphrase)); final key = await pbkdf2.deriveKey( - secretKey: cryptography.SecretKey(text.codeUnits), nonce: 'electrum'.codeUnits); + secretKey: cryptography.SecretKey(text.codeUnits), + nonce: [...'electrum'.codeUnits, ...passphraseBytes]); final bytes = await key.extractBytes(); return Uint8List.fromList(bytes); } diff --git a/cw_bitcoin_cash/lib/src/mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonics_bip39.dart similarity index 65% rename from cw_bitcoin_cash/lib/src/mnemonic.dart rename to cw_bitcoin/lib/bitcoin_mnemonics_bip39.dart index 7aac1d5c4..ff02e875c 100644 --- a/cw_bitcoin_cash/lib/src/mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonics_bip39.dart @@ -7,5 +7,6 @@ class MnemonicBip39 { static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength); /// Create root seed from mnemonic - static Uint8List toSeed(String mnemonic) => bip39.mnemonicToSeed(mnemonic); + static Uint8List toSeed(String mnemonic, {String? passphrase}) => + bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? ''); } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index e2e537ee8..30f04667a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -115,7 +115,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { break; case DerivationType.electrum: default: - seedBytes = await mnemonicToSeedBytes(mnemonic); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } return BitcoinWallet( @@ -195,7 +195,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { switch (walletInfo.derivationInfo!.derivationType) { case DerivationType.electrum: - seedBytes = await mnemonicToSeedBytes(mnemonic); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; case DerivationType.bip39: default: diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 91b8e4ae2..5c276390a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -3,16 +3,18 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class BitcoinNewWalletCredentials extends WalletCredentials { - BitcoinNewWalletCredentials( - {required String name, - WalletInfo? walletInfo, - String? password, - DerivationType? derivationType, - String? derivationPath}) - : super( + BitcoinNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + DerivationType? derivationType, + String? derivationPath, + String? passphrase, + }) : super( name: name, walletInfo: walletInfo, password: password, + passphrase: passphrase, ); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index d6d97f3de..9f67f7807 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +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'; @@ -35,8 +36,21 @@ class BitcoinWalletService extends WalletService< final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; credentials.walletInfo?.network = network.value; + final String mnemonic; + switch ( credentials.walletInfo?.derivationInfo?.derivationType) { + case DerivationType.bip39: + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + mnemonic = await MnemonicBip39.generate(strength: strength); + break; + case DerivationType.electrum: + default: + mnemonic = await generateElectrumMnemonic(); + break; + } + final wallet = await BitcoinWalletBase.create( - mnemonic: await generateElectrumMnemonic(), + mnemonic: mnemonic, password: credentials.password!, passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, diff --git a/cw_bitcoin/lib/electrum_derivations.dart b/cw_bitcoin/lib/electrum_derivations.dart index 749e5c7af..81a3626d2 100644 --- a/cw_bitcoin/lib/electrum_derivations.dart +++ b/cw_bitcoin/lib/electrum_derivations.dart @@ -109,5 +109,4 @@ Map> electrum_derivations = { ], }; - -String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!; \ No newline at end of file +String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index b763b175b..0ce4844c6 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -137,6 +137,8 @@ abstract class ElectrumWalletBase Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1)); final EncryptionFileUtils encryptionFileUtils; + + @override final String? passphrase; @override diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 890d98342..12a43dbe6 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -7,6 +7,7 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -36,6 +37,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required Uint8List seedBytes, required EncryptionFileUtils encryptionFileUtils, + String? passphrase, String? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, @@ -51,6 +53,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, + passphrase: passphrase, currency: CryptoCurrency.ltc) { walletAddresses = LitecoinWalletAddresses( walletInfo, @@ -89,7 +92,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { break; case DerivationType.electrum: default: - seedBytes = await mnemonicToSeedBytes(mnemonic); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } return LitecoinWallet( @@ -100,6 +103,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialAddresses: initialAddresses, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, + passphrase: passphrase, seedBytes: seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, @@ -143,6 +147,31 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } + walletInfo.derivationInfo ??= DerivationInfo(); + + // set the default if not present: + walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; + + Uint8List? seedBytes = null; + final mnemonic = keysData.mnemonic; + final passphrase = keysData.passphrase; + + if (mnemonic != null) { + switch (walletInfo.derivationInfo?.derivationType) { + case DerivationType.bip39: + seedBytes = await bip39.mnemonicToSeed( + mnemonic, + passphrase: passphrase ?? "", + ); + break; + case DerivationType.electrum: + default: + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + break; + } + } + return LitecoinWallet( mnemonic: keysData.mnemonic!, password: password, @@ -150,7 +179,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp?.addresses, initialBalance: snp?.balance, - seedBytes: await mnemonicToSeedBytes(keysData.mnemonic!), + seedBytes: seedBytes!, + passphrase: passphrase, encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index a46b12a2e..c13265934 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; @@ -30,8 +31,21 @@ class LitecoinWalletService extends WalletService< @override Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { + final String mnemonic; + switch ( credentials.walletInfo?.derivationInfo?.derivationType) { + case DerivationType.bip39: + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + mnemonic = await MnemonicBip39.generate(strength: strength); + break; + case DerivationType.electrum: + default: + mnemonic = await generateElectrumMnemonic(); + break; + } + final wallet = await LitecoinWalletBase.create( - mnemonic: await generateElectrumMnemonic(), + mnemonic: mnemonic, password: credentials.password!, passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart index 4699b1649..950b440e7 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart @@ -3,5 +3,4 @@ export 'bitcoin_cash_wallet_addresses.dart'; export 'bitcoin_cash_wallet_creation_credentials.dart'; export 'bitcoin_cash_wallet_service.dart'; export 'exceptions/exceptions.dart'; -export 'mnemonic.dart'; export 'bitcoin_cash_address_utils.dart'; diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 5659528c0..b1e5e7bf6 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -1,13 +1,14 @@ import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -30,6 +31,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required Uint8List seedBytes, required EncryptionFileUtils encryptionFileUtils, + String? passphrase, BitcoinAddressType? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, @@ -45,7 +47,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, currency: CryptoCurrency.bch, - encryptionFileUtils: encryptionFileUtils) { + encryptionFileUtils: encryptionFileUtils, + passphrase: passphrase) { walletAddresses = BitcoinCashWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -67,6 +70,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required EncryptionFileUtils encryptionFileUtils, + String? passphrase, String? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, @@ -79,11 +83,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, - seedBytes: await MnemonicBip39.toSeed(mnemonic), + seedBytes: await MnemonicBip39.toSeed(mnemonic, passphrase: passphrase), encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, + passphrase: passphrase, ); } @@ -150,11 +155,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { } }).toList(), initialBalance: snp?.balance, - seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!), + seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!, passphrase: keysData.passphrase), encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, + passphrase: keysData.passphrase, ); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart index 017040c5d..bb93656f1 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart @@ -2,17 +2,19 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class BitcoinCashNewWalletCredentials extends WalletCredentials { - BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) - : super(name: name, walletInfo: walletInfo, password: password); + BitcoinCashNewWalletCredentials( + {required String name, WalletInfo? walletInfo, String? password, String? passphrase}) + : super(name: name, walletInfo: walletInfo, password: password, passphrase: passphrase); } class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials { - BitcoinCashRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required this.mnemonic, - WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + BitcoinCashRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + String? passphrase, + }) : super(name: name, password: password, walletInfo: walletInfo, passphrase: passphrase); final String mnemonic; } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index a970be261..f004356b5 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:bip39/bip39.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -9,7 +11,6 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:collection/collection.dart'; import 'package:hive/hive.dart'; class BitcoinCashWalletService extends WalletService< @@ -35,11 +36,12 @@ class BitcoinCashWalletService extends WalletService< final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final wallet = await BitcoinCashWalletBase.create( - mnemonic: await MnemonicBip39.generate(strength: strength), + mnemonic: await MnemonicBip39.generate(strength: strength), password: credentials.password!, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + passphrase: credentials.passphrase, ); await wallet.save(); await wallet.init(); @@ -54,11 +56,11 @@ class BitcoinCashWalletService extends WalletService< try { final wallet = await BitcoinCashWalletBase.open( - password: password, - name: name, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource, - encryptionFileUtils: encryptionFileUtilsFor(isDirect), + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); saveBackup(name); @@ -66,11 +68,11 @@ class BitcoinCashWalletService extends WalletService< } catch (_) { await restoreWalletFilesFromBackup(name); final wallet = await BitcoinCashWalletBase.open( - password: password, - name: name, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource, - encryptionFileUtils: encryptionFileUtilsFor(isDirect), + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); return wallet; @@ -130,7 +132,9 @@ class BitcoinCashWalletService extends WalletService< mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, - encryptionFileUtils: encryptionFileUtilsFor(isDirect)); + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + passphrase: credentials.passphrase + ); await wallet.save(); await wallet.init(); return wallet; diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index f55816bb7..48ea90f7c 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -46,6 +46,8 @@ abstract class WalletBase null; + String? get passphrase => null; + Object get keys; WalletAddresses get walletAddresses; diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index d979f9281..c016c1f2a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -29,8 +29,9 @@ class CWBitcoin extends Bitcoin { @override WalletCredentials createBitcoinNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password}) => - BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + {required String name, WalletInfo? walletInfo, String? password, String? passphrase}) => + BitcoinNewWalletCredentials( + name: name, walletInfo: walletInfo, password: password, passphrase: passphrase); @override WalletCredentials createBitcoinHardwareWalletCredentials( @@ -202,8 +203,8 @@ class CWBitcoin extends Bitcoin { await bitcoinWallet.updateAllUnspents(); } - WalletService createBitcoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect) { + WalletService createBitcoinWalletService(Box walletInfoSource, + Box unspentCoinSource, bool alwaysScan, bool isDirect) { return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); } @@ -315,7 +316,7 @@ class CWBitcoin extends Bitcoin { for (DerivationType dType in electrum_derivations.keys) { late Uint8List seedBytes; if (dType == DerivationType.electrum) { - seedBytes = await mnemonicToSeedBytes(mnemonic); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); } else if (dType == DerivationType.bip39) { seedBytes = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? ''); } diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index fcb34a286..62ec1539b 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -15,14 +15,16 @@ class CWBitcoinCash extends BitcoinCash { required String name, WalletInfo? walletInfo, String? password, + String? passphrase, }) => - BitcoinCashNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + BitcoinCashNewWalletCredentials( + name: name, walletInfo: walletInfo, password: password, passphrase: passphrase); @override WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}) => + {required String name, required String mnemonic, required String password, String? passphrase}) => BitcoinCashRestoreWalletFromSeedCredentials( - name: name, mnemonic: mnemonic, password: password); + name: name, mnemonic: mnemonic, password: password, passphrase: passphrase); @override TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) => diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 1e9299282..3ee630b33 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -75,8 +75,10 @@ class WalletCreationService { bool get _hasSeedPhraseLengthOption { switch (type) { - case WalletType.ethereum: + case WalletType.bitcoin: + case WalletType.litecoin: case WalletType.bitcoinCash: + case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: case WalletType.tron: @@ -84,8 +86,6 @@ class WalletCreationService { case WalletType.monero: case WalletType.wownero: case WalletType.none: - case WalletType.bitcoin: - case WalletType.litecoin: case WalletType.haven: case WalletType.nano: case WalletType.banano: diff --git a/lib/di.dart b/lib/di.dart index 1967c9227..8d8c5a368 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -146,7 +146,7 @@ import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dar import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; @@ -179,6 +179,7 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; import 'package:cake_wallet/store/node_list_store.dart'; import 'package:cake_wallet/store/secret_store.dart'; +import 'package:cake_wallet/store/seed_settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/templates/exchange_template_store.dart'; import 'package:cake_wallet/store/templates/send_template_store.dart'; @@ -331,6 +332,7 @@ Future setup({ YatStore(appStore: getIt.get(), secureStorage: getIt.get())..init()); getIt.registerSingleton( AnonpayTransactionsStore(anonpayInvoiceInfoSource: _anonpayInvoiceInfoSource)); + getIt.registerSingleton(SeedSettingsStore()); getIt.registerLazySingleton(() => LedgerViewModel()); @@ -361,6 +363,7 @@ Future setup({ getIt.get(param1: type), _walletInfoSource, getIt.get(param1: type), + getIt.get(), type: type)); getIt.registerFactoryParam((args, closable) { @@ -422,14 +425,21 @@ Future setup({ walletType: args.walletType ?? currentWalletType); }); - getIt.registerFactoryParam((WalletType type, _) { - return WalletRestorationFromQRVM(getIt.get(), - getIt.get(param1: type), _walletInfoSource, type); - }); + getIt.registerFactoryParam((WalletType type, _) => + WalletRestorationFromQRVM( + getIt.get(), + getIt.get(param1: type), + _walletInfoSource, + type, + getIt.get())); getIt.registerFactoryParam((type, _) => - WalletHardwareRestoreViewModel(getIt.get(), getIt.get(), - getIt.get(param1: type), _walletInfoSource, + WalletHardwareRestoreViewModel( + getIt.get(), + getIt.get(), + getIt.get(param1: type), + _walletInfoSource, + getIt.get(), type: type)); getIt.registerFactory(() => WalletAddressListViewModel( @@ -833,7 +843,7 @@ Future setup({ getIt.registerFactory(() => WalletSeedViewModel(getIt.get().wallet!)); - getIt.registerFactory(() => SeedTypeViewModel(getIt.get())); + getIt.registerFactory(() => SeedSettingsViewModel(getIt.get(), getIt.get())); getIt.registerFactoryParam((bool isWalletCreated, _) => WalletSeedPage(getIt.get(), isNewWalletCreated: isWalletCreated)); @@ -1018,12 +1028,12 @@ Future setup({ getIt.registerFactory(() => FaqPage(getIt.get())); getIt.registerFactoryParam((type, _) => - WalletRestoreViewModel( - getIt.get(), getIt.get(param1: type), _walletInfoSource, + WalletRestoreViewModel(getIt.get(), getIt.get(param1: type), + _walletInfoSource, getIt.get(), type: type)); getIt.registerFactoryParam((type, _) => WalletRestorePage( - getIt.get(param1: type), getIt.get())); + getIt.get(param1: type), getIt.get())); getIt.registerFactoryParam, void>( (derivations, _) => WalletRestoreChooseDerivationViewModel(derivationInfos: derivations)); @@ -1275,7 +1285,7 @@ Future setup({ getIt.registerFactory( () => WalletConnectConnectionsView(web3walletService: getIt.get())); - + getIt.registerFactory(() => NFTViewModel(appStore, getIt.get())); getIt.registerFactory(() => TorPage(getIt.get())); diff --git a/lib/entities/language_service.dart b/lib/entities/language_service.dart index 260c8a27f..4a6b358e1 100644 --- a/lib/entities/language_service.dart +++ b/lib/entities/language_service.dart @@ -60,7 +60,7 @@ class LanguageService { 'yo': 'nga', 'ha': 'hau', 'tl': 'phl', - 'hy': 'arm' + 'hy': 'arm', }; static final list = {}; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index e1ee0ada3..743357f92 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -77,6 +77,7 @@ class PreferencesKey { static const exchangeProvidersSelection = 'exchange-providers-selection'; static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status'; static const moneroSeedType = 'monero_seed_type'; + static const bitcoinSeedType = 'bitcoin_seed_type'; static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const donationLinkWalletName = 'donation_link_wallet_name'; diff --git a/lib/entities/seed_type.dart b/lib/entities/seed_type.dart index bc2f6cff7..f941c8a2d 100644 --- a/lib/entities/seed_type.dart +++ b/lib/entities/seed_type.dart @@ -1,18 +1,19 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/enumerable_item.dart'; +import 'package:cw_core/wallet_info.dart'; -class SeedType extends EnumerableItem with Serializable { - const SeedType({required String title, required int raw}) : super(title: title, raw: raw); +class MoneroSeedType extends EnumerableItem with Serializable { + const MoneroSeedType({required String title, required int raw}) : super(title: title, raw: raw); - static const all = [SeedType.legacy, SeedType.polyseed]; + static const all = [MoneroSeedType.legacy, MoneroSeedType.polyseed]; static const defaultSeedType = polyseed; - static const legacy = SeedType(raw: 0, title: 'Legacy (25 words)'); - static const polyseed = SeedType(raw: 1, title: 'Polyseed (16 words)'); - static const wowneroSeed = SeedType(raw: 2, title: 'Wownero (14 words)'); + 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 (14 words)'); - static SeedType deserialize({required int raw}) { + static MoneroSeedType deserialize({required int raw}) { switch (raw) { case 0: return legacy; @@ -28,14 +29,39 @@ class SeedType extends EnumerableItem with Serializable { @override String toString() { switch (this) { - case SeedType.legacy: + case MoneroSeedType.legacy: return S.current.seedtype_legacy; - case SeedType.polyseed: + case MoneroSeedType.polyseed: return S.current.seedtype_polyseed; - case SeedType.wowneroSeed: + case MoneroSeedType.wowneroSeed: return S.current.seedtype_wownero; default: return ''; } } } + +class BitcoinSeedType extends EnumerableItem with Serializable { + const BitcoinSeedType(this.type, {required String title, required int raw}) + : super(title: title, raw: raw); + + final DerivationType type; + + static const all = [BitcoinSeedType.electrum, BitcoinSeedType.bip39]; + + static const defaultDerivationType = bip39; + + static const electrum = BitcoinSeedType(DerivationType.electrum, raw: 0, title: 'Electrum'); + static const bip39 = BitcoinSeedType(DerivationType.bip39, raw: 1, title: 'BIP39'); + + static BitcoinSeedType deserialize({required int raw}) { + switch (raw) { + case 0: + return electrum; + case 1: + return bip39; + default: + throw Exception('Unexpected token: $raw for SeedType deserialize'); + } + } +} diff --git a/lib/router.dart b/lib/router.dart index 25af39043..281d50b69 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -17,6 +17,8 @@ import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/screens/connect_device/select_hardware_wallet_account_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; @@ -27,8 +29,8 @@ import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/sign_page.dart'; 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/disclaimer/disclaimer_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; @@ -43,10 +45,9 @@ import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart'; -import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; -import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; +import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; @@ -69,11 +70,9 @@ import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; -import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; -import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart'; @@ -85,19 +84,18 @@ import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; -import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; +import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_page.dart'; import 'package:cake_wallet/src/screens/welcome/create_welcome_page.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; -import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -106,7 +104,7 @@ import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; @@ -120,7 +118,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; + import 'src/screens/dashboard/pages/nft_import_page.dart'; late RouteSettings currentRouteSettings; @@ -135,7 +133,8 @@ Route createRoute(RouteSettings settings) { case Routes.newWalletFromWelcome: if (SettingsStoreBase.walletPasswordDirectInput) { if (availableWalletTypes.length == 1) { - return createRoute(RouteSettings(name: Routes.newWallet, arguments: availableWalletTypes.first)); + return createRoute( + RouteSettings(name: Routes.newWallet, arguments: availableWalletTypes.first)); } else { return createRoute(RouteSettings(name: Routes.newWalletType)); } @@ -162,10 +161,10 @@ Route createRoute(RouteSettings settings) { case Routes.newWallet: final type = settings.arguments as WalletType; final walletNewVM = getIt.get(param1: type); - final seedTypeViewModel = getIt.get(); + final seedSettingsViewModel = getIt.get(); return CupertinoPageRoute( - builder: (_) => NewWalletPage(walletNewVM, seedTypeViewModel)); + builder: (_) => NewWalletPage(walletNewVM, seedSettingsViewModel)); case Routes.chooseHardwareWalletAccount: final arguments = settings.arguments as List; @@ -348,16 +347,14 @@ Route createRoute(RouteSettings settings) { case Routes.auth: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) - => SettingsStoreBase.walletPasswordDirectInput - ? getIt.get( - param1: WalletUnlockArguments( + builder: (_) => SettingsStoreBase.walletPasswordDirectInput + ? getIt.get( + param1: WalletUnlockArguments( callback: settings.arguments as OnAuthenticationFinished), - instanceName: 'wallet_unlock_verifiable', - param2: true) - : getIt.get( - param1: settings.arguments as OnAuthenticationFinished, - param2: true)); + instanceName: 'wallet_unlock_verifiable', + param2: true) + : getIt.get( + param1: settings.arguments as OnAuthenticationFinished, param2: true)); case Routes.totpAuthCodePage: final args = settings.arguments as TotpAuthArgumentsModel; @@ -371,28 +368,25 @@ Route createRoute(RouteSettings settings) { case Routes.walletUnlockLoadable: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) - => getIt.get( + builder: (_) => getIt.get( param1: settings.arguments as WalletUnlockArguments, - instanceName: 'wallet_unlock_loadable', + instanceName: 'wallet_unlock_loadable', param2: true)); case Routes.unlock: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) - => SettingsStoreBase.walletPasswordDirectInput - ? WillPopScope( - child: getIt.get( + builder: (_) => SettingsStoreBase.walletPasswordDirectInput + ? WillPopScope( + child: getIt.get( param1: WalletUnlockArguments( - callback: settings.arguments as OnAuthenticationFinished), + callback: settings.arguments as OnAuthenticationFinished), param2: false, instanceName: 'wallet_unlock_verifiable'), - onWillPop: () async => false) - : WillPopScope( - child: getIt.get( - param1: settings.arguments as OnAuthenticationFinished, - param2: false), + onWillPop: () async => false) + : WillPopScope( + child: getIt.get( + param1: settings.arguments as OnAuthenticationFinished, param2: false), onWillPop: () async => false)); case Routes.silentPaymentsSettings: @@ -437,11 +431,12 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( builder: (context) => WillPopScope( child: SettingsStoreBase.walletPasswordDirectInput - ? getIt.get(instanceName: 'wallet_password_login') - : getIt.get(instanceName: 'login'), + ? getIt.get(instanceName: 'wallet_password_login') + : getIt.get(instanceName: 'login'), onWillPop: () async => - // FIX-ME: Additional check does it works correctly - (await SystemChannels.platform.invokeMethod('SystemNavigator.pop') ?? false)), + // FIX-ME: Additional check does it works correctly + (await SystemChannels.platform.invokeMethod('SystemNavigator.pop') ?? + false)), fullscreenDialog: true); case Routes.newPowNode: @@ -537,8 +532,7 @@ Route createRoute(RouteSettings settings) { case Routes.support: return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get()); + fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.supportLiveChat: return CupertinoPageRoute(builder: (_) => getIt.get()); @@ -567,8 +561,7 @@ Route createRoute(RouteSettings settings) { case Routes.cakePayBuyCardPage: final args = settings.arguments as List; - return CupertinoPageRoute( - builder: (_) => getIt.get(param1: args)); + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); case Routes.cakePayBuyCardDetailPage: final args = settings.arguments as List; @@ -582,7 +575,8 @@ Route createRoute(RouteSettings settings) { case Routes.cakePayVerifyOtpPage: final args = settings.arguments as List; - return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); + return CupertinoPageRoute( + builder: (_) => getIt.get(param1: args)); case Routes.cakePayAccountPage: return CupertinoPageRoute(builder: (_) => getIt.get()); @@ -597,16 +591,19 @@ Route createRoute(RouteSettings settings) { case Routes.advancedPrivacySettings: final args = settings.arguments as Map; final type = args['type'] as WalletType; + final isFromRestore = args['isFromRestore'] as bool? ?? false; final useTestnet = args['useTestnet'] as bool; final toggleTestnet = args['toggleTestnet'] as Function(bool? val); return CupertinoPageRoute( builder: (_) => AdvancedPrivacySettingsPage( - useTestnet, - toggleTestnet, - getIt.get(param1: type), - getIt.get(param1: type, param2: false), - getIt.get(), + isFromRestore: isFromRestore, + useTestnet: useTestnet, + toggleUseTestnet: toggleTestnet, + advancedPrivacySettingsViewModel: + getIt.get(param1: type), + nodeViewModel: getIt.get(param1: type, param2: false), + seedSettingsViewModel: getIt.get(), )); case Routes.anonPayInvoicePage: @@ -709,7 +706,7 @@ Route createRoute(RouteSettings settings) { getIt.get(), ), ); - + case Routes.connectDevices: final params = settings.arguments as ConnectDevicePageParams; return MaterialPageRoute( diff --git a/lib/src/screens/dashboard/widgets/verify_form.dart b/lib/src/screens/dashboard/widgets/verify_form.dart index d59261494..bf6809586 100644 --- a/lib/src/screens/dashboard/widgets/verify_form.dart +++ b/lib/src/screens/dashboard/widgets/verify_form.dart @@ -1,23 +1,7 @@ -import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/entities/generate_name.dart'; -import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/address_text_field.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; -import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; -import 'package:cake_wallet/src/widgets/seed_widget.dart'; -import 'package:cake_wallet/themes/extensions/address_theme.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -import 'package:cake_wallet/utils/show_bar.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:mobx/mobx.dart'; -import 'package:polyseed/polyseed.dart'; class VerifyForm extends StatefulWidget { VerifyForm({ 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 881d9f95a..ff8ec3dd2 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -3,52 +3,61 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/seed_phrase_length.dart'; import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; -import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; -import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; -import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; -import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; +import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; class AdvancedPrivacySettingsPage extends BasePage { - AdvancedPrivacySettingsPage(this.useTestnet, this.toggleUseTestnet, - this.advancedPrivacySettingsViewModel, this.nodeViewModel, this.seedTypeViewModel); + AdvancedPrivacySettingsPage({ + required this.isFromRestore, + required this.useTestnet, + required this.toggleUseTestnet, + required this.advancedPrivacySettingsViewModel, + required this.nodeViewModel, + required this.seedSettingsViewModel, + }); final AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel; final NodeCreateOrEditViewModel nodeViewModel; - final SeedTypeViewModel seedTypeViewModel; + final SeedSettingsViewModel seedSettingsViewModel; @override String get title => S.current.privacy_settings; + final bool isFromRestore; final bool useTestnet; final Function(bool? val) toggleUseTestnet; @override - Widget body(BuildContext context) => AdvancedPrivacySettingsBody(useTestnet, toggleUseTestnet, - advancedPrivacySettingsViewModel, nodeViewModel, seedTypeViewModel); + Widget body(BuildContext context) => _AdvancedPrivacySettingsBody(isFromRestore, useTestnet, + toggleUseTestnet, advancedPrivacySettingsViewModel, nodeViewModel, seedSettingsViewModel); } -class AdvancedPrivacySettingsBody extends StatefulWidget { - const AdvancedPrivacySettingsBody(this.useTestnet, this.toggleUseTestnet, +class _AdvancedPrivacySettingsBody extends StatefulWidget { + const _AdvancedPrivacySettingsBody(this.isFromRestore, this.useTestnet, this.toggleUseTestnet, this.privacySettingsViewModel, this.nodeViewModel, this.seedTypeViewModel, {Key? key}) : super(key: key); final AdvancedPrivacySettingsViewModel privacySettingsViewModel; final NodeCreateOrEditViewModel nodeViewModel; - final SeedTypeViewModel seedTypeViewModel; + final SeedSettingsViewModel seedTypeViewModel; + final bool isFromRestore; final bool useTestnet; final Function(bool? val) toggleUseTestnet; @@ -56,15 +65,23 @@ class AdvancedPrivacySettingsBody extends StatefulWidget { _AdvancedPrivacySettingsBodyState createState() => _AdvancedPrivacySettingsBodyState(); } -class _AdvancedPrivacySettingsBodyState extends State { - _AdvancedPrivacySettingsBodyState(); - +class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBody> { + final TextEditingController passphraseController = TextEditingController(); final _formKey = GlobalKey(); bool? testnetValue; + @override + void initState() { + passphraseController.text = widget.seedTypeViewModel.passphrase ?? ''; + + passphraseController + .addListener(() => widget.seedTypeViewModel.setPassphrase(passphraseController.text)); + super.initState(); + } + @override Widget build(BuildContext context) { - if (testnetValue == null && widget.useTestnet != null) { + if (testnetValue == null && widget.useTestnet) { testnetValue = widget.useTestnet; } @@ -97,6 +114,52 @@ class _AdvancedPrivacySettingsBodyState extends State( + title: S.current.seedtype, + items: MoneroSeedType.all, + selectedItem: widget.seedTypeViewModel.moneroSeedType, + onItemSelected: widget.seedTypeViewModel.setMoneroSeedType, + ), + ); + }), + if ([WalletType.bitcoin, WalletType.litecoin] + .contains(widget.privacySettingsViewModel.type)) + Observer(builder: (_) { + return SettingsChoicesCell( + ChoicesListItem( + title: S.current.seedtype, + items: BitcoinSeedType.all, + selectedItem: widget.seedTypeViewModel.bitcoinSeedType, + onItemSelected: widget.seedTypeViewModel.setBitcoinSeedType, + ), + ); + }), + if (!widget.isFromRestore) ...[ + Observer(builder: (_) { + if (widget.privacySettingsViewModel.hasSeedPhraseLengthOption) + return SettingsPickerCell( + title: S.current.seed_phrase_length, + items: SeedPhraseLength.values, + selectedItem: widget.privacySettingsViewModel.seedPhraseLength, + onItemSelected: (SeedPhraseLength length) { + widget.privacySettingsViewModel.setSeedPhraseLength(length); + }, + ); + return Container(); + }), + if (widget.privacySettingsViewModel.hasPassphraseOption) + Padding( + padding: EdgeInsets.all(24), + child: BaseTextFormField( + hintText: S.current.passphrase, + controller: passphraseController, + obscureText: true, + ), + ), + ], Observer(builder: (_) { return Column( children: [ @@ -122,31 +185,9 @@ class _AdvancedPrivacySettingsBodyState extends State( - title: S.current.seed_phrase_length, - items: SeedPhraseLength.values, - selectedItem: widget.privacySettingsViewModel.seedPhraseLength, - onItemSelected: (SeedPhraseLength length) { - widget.privacySettingsViewModel.setSeedPhraseLength(length); - }, - ); - }), - if (widget.privacySettingsViewModel.hasSeedTypeOption) - Observer(builder: (_) { - return SettingsChoicesCell( - ChoicesListItem( - title: S.current.seedtype, - items: SeedType.all, - selectedItem: widget.seedTypeViewModel.moneroSeedType, - onItemSelected: widget.seedTypeViewModel.setMoneroSeedType, - ), - ); - }), if (widget.privacySettingsViewModel.type == WalletType.bitcoin) Builder(builder: (_) { - final val = testnetValue!; + final val = testnetValue ?? false; return SettingsSwitcherCell( title: S.current.use_testnet, value: val, @@ -154,7 +195,7 @@ class _AdvancedPrivacySettingsBodyState extends State widget.seedTypeViewModel.setPassphrase(passphraseController.text)); + super.dispose(); + } } diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 471240877..8f61ebb38 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -1,36 +1,35 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/core/wallet_name_validator.dart'; import 'package:cake_wallet/entities/generate_name.dart'; -import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/routes.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/seed_type_view_model.dart'; -import 'package:mobx/mobx.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/src/widgets/seed_language_selector.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/view_model/wallet_new_vm.dart'; +import 'package:cake_wallet/src/widgets/seed_language_selector.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -import 'package:cake_wallet/entities/seed_type.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/seed_settings_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; class NewWalletPage extends BasePage { - NewWalletPage(this._walletNewVM, this._seedTypeViewModel); + NewWalletPage(this._walletNewVM, this._seedSettingsViewModel); final WalletNewVM _walletNewVM; - final SeedTypeViewModel _seedTypeViewModel; + final SeedSettingsViewModel _seedSettingsViewModel; final walletNameImage = Image.asset('assets/images/wallet_name.png'); @@ -51,15 +50,15 @@ class NewWalletPage extends BasePage { Widget body(BuildContext context) => WalletNameForm( _walletNewVM, currentTheme.type == ThemeType.dark ? walletNameImage : walletNameLightImage, - _seedTypeViewModel); + _seedSettingsViewModel); } class WalletNameForm extends StatefulWidget { - WalletNameForm(this._walletNewVM, this.walletImage, this._seedTypeViewModel); + WalletNameForm(this._walletNewVM, this.walletImage, this._seedSettingsViewModel); final WalletNewVM _walletNewVM; final Image walletImage; - final SeedTypeViewModel _seedTypeViewModel; + final SeedSettingsViewModel _seedSettingsViewModel; @override _WalletNameFormState createState() => _WalletNameFormState(_walletNewVM); @@ -110,7 +109,7 @@ class _WalletNameFormState extends State { } }); - _setSeedType(SeedType.defaultSeedType); + _setSeedType(MoneroSeedType.defaultSeedType); super.initState(); } @@ -285,12 +284,12 @@ class _WalletNameFormState extends State { builder: (BuildContext build) => Padding( padding: EdgeInsets.only(top: 24), child: SelectButton( - text: widget._seedTypeViewModel.moneroSeedType.title, + text: widget._seedSettingsViewModel.moneroSeedType.title, onTap: () async { await showPopUp( context: context, builder: (_) => Picker( - items: SeedType.all, + items: MoneroSeedType.all, selectedAtIndex: isPolyseed ? 1 : 0, onItemSelected: _setSeedType, isSeparated: false, @@ -308,8 +307,8 @@ class _WalletNameFormState extends State { key: _languageSelectorKey, initialSelected: defaultSeedLanguage, seedType: _walletNewVM.hasSeedType - ? widget._seedTypeViewModel.moneroSeedType - : SeedType.legacy, + ? widget._seedSettingsViewModel.moneroSeedType + : MoneroSeedType.legacy, ), ), ) @@ -380,10 +379,10 @@ class _WalletNameFormState extends State { _formProcessing = false; } - bool get isPolyseed => widget._seedTypeViewModel.moneroSeedType == SeedType.polyseed; + bool get isPolyseed => widget._seedSettingsViewModel.moneroSeedType == MoneroSeedType.polyseed; - void _setSeedType(SeedType item) { - widget._seedTypeViewModel.setMoneroSeedType(item); + void _setSeedType(MoneroSeedType item) { + widget._seedSettingsViewModel.setMoneroSeedType(item); _languageSelectorKey.currentState?.selected = defaultSeedLanguage; // Reset Seed language } } 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 ec40eb1c1..f295aab13 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -9,35 +9,34 @@ import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; import 'package:cake_wallet/src/widgets/seed_widget.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; import 'package:polyseed/polyseed.dart'; class WalletRestoreFromSeedForm extends StatefulWidget { - WalletRestoreFromSeedForm( - {Key? key, - required this.displayLanguageSelector, - required this.displayBlockHeightSelector, - required this.displayPassphrase, - required this.type, - required this.displayWalletPassword, - required this.seedTypeViewModel, - this.blockHeightFocusNode, - this.onHeightOrDateEntered, - this.onSeedChange, - this.onLanguageChange, - this.onPasswordChange, - this.onRepeatedPasswordChange}) - : super(key: key); + WalletRestoreFromSeedForm({Key? key, + required this.displayLanguageSelector, + required this.displayBlockHeightSelector, + required this.displayPassphrase, + required this.type, + required this.displayWalletPassword, + required this.seedSettingsViewModel, + this.blockHeightFocusNode, + this.onHeightOrDateEntered, + this.onSeedChange, + this.onLanguageChange, + this.onPasswordChange, + this.onRepeatedPasswordChange, + }) : super(key: key); final WalletType type; final bool displayLanguageSelector; final bool displayBlockHeightSelector; final bool displayWalletPassword; final bool displayPassphrase; - final SeedTypeViewModel seedTypeViewModel; + final SeedSettingsViewModel seedSettingsViewModel; final FocusNode? blockHeightFocusNode; final Function(bool)? onHeightOrDateEntered; final void Function(String)? onSeedChange; @@ -58,7 +57,9 @@ class WalletRestoreFromSeedFormState extends State { languageController = TextEditingController(), nameTextEditingController = TextEditingController(), passwordTextEditingController = displayWalletPassword ? TextEditingController() : null, - repeatedPasswordTextEditingController = displayWalletPassword ? TextEditingController() : null, + repeatedPasswordTextEditingController = displayWalletPassword + ? TextEditingController() + : null, passphraseController = TextEditingController(), seedTypeController = TextEditingController(); @@ -75,10 +76,11 @@ class WalletRestoreFromSeedFormState extends State { String language; void Function()? passwordListener; void Function()? repeatedPasswordListener; + void Function()? passphraseListener; @override void initState() { - _setSeedType(widget.seedTypeViewModel.moneroSeedType); + _setSeedType(widget.seedSettingsViewModel.moneroSeedType); _setLanguageLabel(language); if (passwordTextEditingController != null) { @@ -87,14 +89,19 @@ class WalletRestoreFromSeedFormState extends State { } if (repeatedPasswordTextEditingController != null) { - repeatedPasswordListener = () => widget.onRepeatedPasswordChange?.call(repeatedPasswordTextEditingController!.text); + repeatedPasswordListener = + () => widget.onRepeatedPasswordChange?.call(repeatedPasswordTextEditingController!.text); repeatedPasswordTextEditingController?.addListener(repeatedPasswordListener!); } + + passphraseListener = () => widget.seedSettingsViewModel.setPassphrase(passphraseController.text); + passphraseController.addListener(passphraseListener!); + moneroSeedTypeReaction = - reaction((_) => widget.seedTypeViewModel.moneroSeedType, (SeedType item) { - _setSeedType(item); - _changeLanguage('English'); - }); + reaction((_) => widget.seedSettingsViewModel.moneroSeedType, (MoneroSeedType item) { + _setSeedType(item); + _changeLanguage('English'); + }); super.initState(); } @@ -110,6 +117,9 @@ class WalletRestoreFromSeedFormState extends State { if (repeatedPasswordListener != null) { repeatedPasswordTextEditingController?.removeListener(repeatedPasswordListener!); } + + passphraseController.removeListener(passphraseListener!); + super.dispose(); } @@ -118,11 +128,13 @@ class WalletRestoreFromSeedFormState extends State { Polyseed.isValidSeed(seed)) { final lang = PolyseedLang.getByPhrase(seed); - _changeSeedType(SeedType.polyseed); + _changeSeedType(MoneroSeedType.polyseed); _changeLanguage(lang.nameEnglish); } - if (widget.type == WalletType.wownero && seed.split(" ").length == 14) { - _changeSeedType(SeedType.wowneroSeed); + if (widget.type == WalletType.wownero && seed + .split(" ") + .length == 14) { + _changeSeedType(MoneroSeedType.wowneroSeed); _changeLanguage("English"); } widget.onSeedChange?.call(seed); @@ -140,7 +152,9 @@ class WalletRestoreFromSeedFormState extends State { children: [ BaseTextFormField( controller: nameTextEditingController, - hintText: S.of(context).wallet_name, + hintText: S + .of(context) + .wallet_name, suffixIcon: IconButton( onPressed: () async { final rName = await generateName(); @@ -156,7 +170,9 @@ class WalletRestoreFromSeedFormState extends State { padding: const EdgeInsets.all(8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), - color: Theme.of(context).hintColor, + color: Theme + .of(context) + .hintColor, ), width: 34, height: 34, @@ -183,13 +199,14 @@ class WalletRestoreFromSeedFormState extends State { onTap: () async { await showPopUp( context: context, - builder: (_) => Picker( + builder: (_) => + Picker( items: _getItems(), selectedAtIndex: isPolyseed ? 1 : seedTypeController.value.text.contains("14") - ? 2 - : 0, + ? 2 + : 0, mainAxisAlignment: MainAxisAlignment.start, onItemSelected: _changeSeedType, isSeparated: false, @@ -211,37 +228,43 @@ class WalletRestoreFromSeedFormState extends State { if (widget.displayWalletPassword) ...[BaseTextFormField( controller: passwordTextEditingController, - hintText: S.of(context).password, + hintText: S + .of(context) + .password, obscureText: true), BaseTextFormField( controller: repeatedPasswordTextEditingController, - hintText: S.of(context).repeat_wallet_password, - obscureText: true)], + hintText: S + .of(context) + .repeat_wallet_password, + obscureText: true) + ], if (widget.displayLanguageSelector) - if (!seedTypeController.value.text.contains("14") && widget.displayLanguageSelector) - GestureDetector( - onTap: () async { - await showPopUp( - context: context, - builder: (_) => SeedLanguagePicker( - selected: language, - onItemSelected: _changeLanguage, - seedType: isPolyseed ? SeedType.polyseed : SeedType.legacy, - )); - }, - child: Container( - color: Colors.transparent, - padding: EdgeInsets.only(top: 20.0), - child: IgnorePointer( - child: BaseTextFormField( - controller: languageController, - enableInteractiveSelection: false, - readOnly: true, - suffixIcon: expandIcon, + if (!seedTypeController.value.text.contains("14") && widget.displayLanguageSelector) + GestureDetector( + onTap: () async { + await showPopUp( + context: context, + builder: (_) => + SeedLanguagePicker( + selected: language, + onItemSelected: _changeLanguage, + seedType: isPolyseed ? MoneroSeedType.polyseed : MoneroSeedType.legacy, + )); + }, + child: Container( + color: Colors.transparent, + padding: EdgeInsets.only(top: 20.0), + child: IgnorePointer( + child: BaseTextFormField( + controller: languageController, + enableInteractiveSelection: false, + readOnly: true, + suffixIcon: expandIcon, + ), ), ), ), - ), if ((!isPolyseed) && widget.displayBlockHeightSelector) BlockchainHeightWidget( focusNode: widget.blockHeightFocusNode, @@ -262,17 +285,20 @@ class WalletRestoreFromSeedFormState extends State { } bool get isPolyseed => - widget.seedTypeViewModel.moneroSeedType == SeedType.polyseed && - (widget.type == WalletType.monero || widget.type == WalletType.wownero); + widget.seedSettingsViewModel.moneroSeedType == MoneroSeedType.polyseed && + (widget.type == WalletType.monero || widget.type == WalletType.wownero); - Widget get expandIcon => Container( + Widget get expandIcon => + Container( padding: EdgeInsets.all(18), width: 24, height: 24, child: Image.asset( 'assets/images/arrow_bottom_purple_icon.png', height: 8, - color: Theme.of(context).hintColor, + color: Theme + .of(context) + .hintColor, ), ); @@ -280,8 +306,8 @@ class WalletRestoreFromSeedFormState extends State { final setLang = isPolyseed ? "POLYSEED_$language" : seedTypeController.value.text.contains("14") - ? "WOWSEED_" + language - : language; + ? "WOWSEED_" + language + : language; setState(() { this.language = setLang; seedWidgetStateKey.currentState!.changeSeedLanguage(setLang); @@ -293,24 +319,24 @@ class WalletRestoreFromSeedFormState extends State { void _setLanguageLabel(String language) => languageController.text = '${language.replaceAll("POLYSEED_", "")} (Seed language)'; - void _changeSeedType(SeedType item) { + void _changeSeedType(MoneroSeedType item) { _setSeedType(item); _changeLanguage('English'); - widget.seedTypeViewModel.setMoneroSeedType(item); + widget.seedSettingsViewModel.setMoneroSeedType(item); } - void _setSeedType(SeedType item) { + void _setSeedType(MoneroSeedType item) { seedTypeController.text = item.toString(); } - List _getItems() { + List _getItems() { switch (widget.type) { case WalletType.monero: - return [SeedType.legacy, SeedType.polyseed]; + return [MoneroSeedType.legacy, MoneroSeedType.polyseed]; case WalletType.wownero: - return [SeedType.legacy, SeedType.polyseed, SeedType.wowneroSeed]; + return [MoneroSeedType.legacy, MoneroSeedType.polyseed, MoneroSeedType.wowneroSeed]; default: - return [SeedType.legacy]; + return [MoneroSeedType.legacy]; } } } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index c8fa3665e..741befbc0 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -12,7 +12,7 @@ import 'package:cake_wallet/themes/extensions/wallet_list_theme.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'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -23,7 +23,7 @@ import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; class WalletRestorePage extends BasePage { - WalletRestorePage(this.walletRestoreViewModel, this.seedTypeViewModel) + WalletRestorePage(this.walletRestoreViewModel, this.seedSettingsViewModel) : walletRestoreFromSeedFormKey = GlobalKey(), walletRestoreFromKeysFormKey = GlobalKey(), _pages = [], @@ -33,7 +33,7 @@ class WalletRestorePage extends BasePage { switch (mode) { case WalletRestoreMode.seed: _pages.add(WalletRestoreFromSeedForm( - seedTypeViewModel: seedTypeViewModel, + seedSettingsViewModel: seedSettingsViewModel, displayBlockHeightSelector: walletRestoreViewModel.hasBlockchainHeightLanguageSelector, displayLanguageSelector: walletRestoreViewModel.hasSeedLanguageSelector, @@ -96,7 +96,7 @@ class WalletRestorePage extends BasePage { )); final WalletRestoreViewModel walletRestoreViewModel; - final SeedTypeViewModel seedTypeViewModel; + final SeedSettingsViewModel seedSettingsViewModel; final PageController _controller; final List _pages; final GlobalKey walletRestoreFromSeedFormKey; @@ -233,6 +233,7 @@ class WalletRestorePage extends BasePage { onTap: () { Navigator.of(context) .pushNamed(Routes.advancedPrivacySettings, arguments: { + 'isFromRestore': true, 'type': walletRestoreViewModel.type, 'useTestnet': walletRestoreViewModel.useTestnet, 'toggleTestnet': walletRestoreViewModel.toggleUseTestnet @@ -322,8 +323,7 @@ class WalletRestorePage extends BasePage { } if (walletRestoreViewModel.hasPassphrase) { - credentials['passphrase'] = - walletRestoreFromSeedFormKey.currentState!.passphraseController.text; + credentials['passphrase'] = seedSettingsViewModel.passphrase; } credentials['name'] = @@ -426,6 +426,7 @@ class WalletRestorePage extends BasePage { } await walletRestoreViewModel.create(options: _credentials()); + seedSettingsViewModel.setPassphrase(null); } catch (e) { _formProcessing = false; rethrow; diff --git a/lib/src/widgets/seed_language_picker.dart b/lib/src/widgets/seed_language_picker.dart index 5cb8f2fd3..4a63e3092 100644 --- a/lib/src/widgets/seed_language_picker.dart +++ b/lib/src/widgets/seed_language_picker.dart @@ -11,36 +11,36 @@ class SeedLanguagePickerOption { final String name; final String nameLocalized; final Image image; - final List supportedSeedTypes; + final List supportedSeedTypes; } final List seedLanguages = [ SeedLanguagePickerOption('English', S.current.seed_language_english, - Image.asset('assets/images/flags/usa.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/usa.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Chinese (Simplified)', S.current.seed_language_chinese, - Image.asset('assets/images/flags/chn.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/chn.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Chinese (Traditional)', S.current.seed_language_chinese_traditional, - Image.asset('assets/images/flags/chn.png'), [SeedType.polyseed]), + Image.asset('assets/images/flags/chn.png'), [MoneroSeedType.polyseed]), SeedLanguagePickerOption('Dutch', S.current.seed_language_dutch, - Image.asset('assets/images/flags/nld.png'), [SeedType.legacy]), + Image.asset('assets/images/flags/nld.png'), [MoneroSeedType.legacy]), SeedLanguagePickerOption('German', S.current.seed_language_german, - Image.asset('assets/images/flags/deu.png'), [SeedType.legacy]), + Image.asset('assets/images/flags/deu.png'), [MoneroSeedType.legacy]), SeedLanguagePickerOption('Japanese', S.current.seed_language_japanese, - Image.asset('assets/images/flags/jpn.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/jpn.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Korean', S.current.seed_language_korean, - Image.asset('assets/images/flags/kor.png'), [SeedType.polyseed]), + Image.asset('assets/images/flags/kor.png'), [MoneroSeedType.polyseed]), SeedLanguagePickerOption('Portuguese', S.current.seed_language_portuguese, - Image.asset('assets/images/flags/prt.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/prt.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Russian', S.current.seed_language_russian, - Image.asset('assets/images/flags/rus.png'), [SeedType.legacy]), + Image.asset('assets/images/flags/rus.png'), [MoneroSeedType.legacy]), SeedLanguagePickerOption('Czech', S.current.seed_language_czech, - Image.asset('assets/images/flags/czk.png'), [SeedType.polyseed]), + Image.asset('assets/images/flags/czk.png'), [MoneroSeedType.polyseed]), SeedLanguagePickerOption('Spanish', S.current.seed_language_spanish, - Image.asset('assets/images/flags/esp.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/esp.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('French', S.current.seed_language_french, - Image.asset('assets/images/flags/fra.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/fra.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Italian', S.current.seed_language_italian, - Image.asset('assets/images/flags/ita.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/ita.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), ]; const defaultSeedLanguage = 'English'; @@ -51,11 +51,11 @@ class SeedLanguagePicker extends StatefulWidget { SeedLanguagePicker( {Key? key, this.selected = defaultSeedLanguage, - this.seedType = SeedType.defaultSeedType, + this.seedType = MoneroSeedType.defaultSeedType, required this.onItemSelected}) : super(key: key); - final SeedType seedType; + final MoneroSeedType seedType; final String selected; final Function(String) onItemSelected; @@ -68,7 +68,7 @@ class SeedLanguagePickerState extends State { SeedLanguagePickerState( {required this.selected, required this.onItemSelected, required this.seedType}); - final SeedType seedType; + final MoneroSeedType seedType; final String selected; final Function(String) onItemSelected; diff --git a/lib/src/widgets/seed_language_selector.dart b/lib/src/widgets/seed_language_selector.dart index dde78c58c..87f3aa573 100644 --- a/lib/src/widgets/seed_language_selector.dart +++ b/lib/src/widgets/seed_language_selector.dart @@ -7,11 +7,11 @@ import 'package:flutter/material.dart'; class SeedLanguageSelector extends StatefulWidget { SeedLanguageSelector( - {Key? key, required this.initialSelected, this.seedType = SeedType.defaultSeedType}) + {Key? key, required this.initialSelected, this.seedType = MoneroSeedType.defaultSeedType}) : super(key: key); final String initialSelected; - final SeedType seedType; + final MoneroSeedType seedType; @override SeedLanguageSelectorState createState() => SeedLanguageSelectorState(selected: initialSelected); diff --git a/lib/store/seed_settings_store.dart b/lib/store/seed_settings_store.dart new file mode 100644 index 000000000..90c02ba97 --- /dev/null +++ b/lib/store/seed_settings_store.dart @@ -0,0 +1,11 @@ +import 'package:mobx/mobx.dart'; + +part 'seed_settings_store.g.dart'; + +class SeedSettingsStore = SeedSettingsStoreBase with _$SeedSettingsStore; + +abstract class SeedSettingsStoreBase with Store { + + @observable + String? passphrase; +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index ee145195c..ea5f43d98 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,45 +1,46 @@ import 'dart:io'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/action_list_display_mode.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; -import 'package:cake_wallet/entities/provider_types.dart'; -import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; +import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/entities/seed_phrase_length.dart'; import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; -import 'package:cake_wallet/polygon/polygon.dart'; -import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; -import 'package:cake_wallet/view_model/settings/sync_mode.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cake_wallet/wownero/wownero.dart'; -import 'package:cw_core/transaction_priority.dart'; +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/package_info.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/set_app_secure_native.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/utils/package_info.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/entities/language_service.dart'; -import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/entities/fiat_currency.dart'; -import 'package:cw_core/node.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/entities/action_list_display_mode.dart'; -import 'package:cake_wallet/entities/fiat_api_mode.dart'; -import 'package:cw_core/set_app_secure_native.dart'; part 'settings_store.g.dart'; @@ -55,7 +56,8 @@ abstract class SettingsStoreBase with Store { required BalanceDisplayMode initialBalanceDisplayMode, required bool initialSaveRecipientAddress, required AutoGenerateSubaddressStatus initialAutoGenerateSubaddressStatus, - required SeedType initialMoneroSeedType, + required MoneroSeedType initialMoneroSeedType, + required BitcoinSeedType initialBitcoinSeedType, required bool initialAppSecure, required bool initialDisableBuy, required bool initialDisableSell, @@ -128,6 +130,7 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress = initialSaveRecipientAddress, autoGenerateSubaddressStatus = initialAutoGenerateSubaddressStatus, moneroSeedType = initialMoneroSeedType, + bitcoinSeedType = initialBitcoinSeedType, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, selectedCake2FAPreset = initialCake2FAPresetOptions, @@ -329,9 +332,14 @@ abstract class SettingsStoreBase with Store { reaction( (_) => moneroSeedType, - (SeedType moneroSeedType) => + (MoneroSeedType moneroSeedType) => sharedPreferences.setInt(PreferencesKey.moneroSeedType, moneroSeedType.raw)); + reaction( + (_) => bitcoinSeedType, + (BitcoinSeedType bitcoinSeedType) => sharedPreferences.setInt( + PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); + reaction( (_) => fiatApiMode, (FiatApiMode mode) => @@ -555,7 +563,8 @@ abstract class SettingsStoreBase with Store { static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; static final walletPasswordDirectInput = Platform.isLinux; static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; - static const defaultMoneroSeedType = SeedType.defaultSeedType; + static const defaultMoneroSeedType = MoneroSeedType.defaultSeedType; + static const defaultBitcoinSeedType = BitcoinSeedType.defaultDerivationType; @observable FiatCurrency fiatCurrency; @@ -585,7 +594,10 @@ abstract class SettingsStoreBase with Store { AutoGenerateSubaddressStatus autoGenerateSubaddressStatus; @observable - SeedType moneroSeedType; + MoneroSeedType moneroSeedType; + + @observable + BitcoinSeedType bitcoinSeedType; @observable bool isAppSecure; @@ -945,9 +957,15 @@ abstract class SettingsStoreBase with Store { final _moneroSeedType = sharedPreferences.getInt(PreferencesKey.moneroSeedType); final moneroSeedType = _moneroSeedType != null - ? SeedType.deserialize(raw: _moneroSeedType) + ? MoneroSeedType.deserialize(raw: _moneroSeedType) : defaultMoneroSeedType; + final _bitcoinSeedType = sharedPreferences.getInt(PreferencesKey.bitcoinSeedType); + + final bitcoinSeedType = _bitcoinSeedType != null + ? BitcoinSeedType.deserialize(raw: _bitcoinSeedType) + : defaultBitcoinSeedType; + final nodes = {}; final powNodes = {}; @@ -1111,6 +1129,7 @@ abstract class SettingsStoreBase with Store { initialSaveRecipientAddress: shouldSaveRecipientAddress, initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, initialMoneroSeedType: moneroSeedType, + initialBitcoinSeedType: bitcoinSeedType, initialAppSecure: isAppSecure, initialDisableBuy: disableBuy, initialDisableSell: disableSell, @@ -1233,9 +1252,15 @@ abstract class SettingsStoreBase with Store { final _moneroSeedType = sharedPreferences.getInt(PreferencesKey.moneroSeedType); moneroSeedType = _moneroSeedType != null - ? SeedType.deserialize(raw: _moneroSeedType) + ? MoneroSeedType.deserialize(raw: _moneroSeedType) : defaultMoneroSeedType; + final _bitcoinSeedType = sharedPreferences.getInt(PreferencesKey.bitcoinSeedType); + + bitcoinSeedType = _bitcoinSeedType != null + ? BitcoinSeedType.deserialize(raw: _bitcoinSeedType) + : defaultBitcoinSeedType; + balanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); shouldSaveRecipientAddress = diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 73308f15a..da294cdea 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -30,6 +30,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { final SettingsStore _settingsStore; + @computed bool get hasSeedPhraseLengthOption { // convert to switch case so that it give a syntax error when adding a new wallet type // thus we don't forget about it @@ -40,11 +41,14 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.solana: case WalletType.tron: return true; + + case WalletType.bitcoin: + case WalletType.litecoin: + return _settingsStore.bitcoinSeedType == BitcoinSeedType.bip39; + case WalletType.monero: case WalletType.wownero: case WalletType.none: - case WalletType.bitcoin: - case WalletType.litecoin: case WalletType.haven: case WalletType.nano: case WalletType.banano: @@ -52,7 +56,13 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { } } - bool get hasSeedTypeOption => type == WalletType.monero || type == WalletType.wownero; + bool get hasSeedTypeOption => [WalletType.monero, WalletType.wownero].contains(type); + + bool get hasPassphraseOption => [ + WalletType.bitcoin, + WalletType.litecoin, + WalletType.bitcoinCash, + ].contains(type); @computed bool get addCustomNode => _addCustomNode; @@ -61,7 +71,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { SeedPhraseLength get seedPhraseLength => _settingsStore.seedPhraseLength; @computed - bool get isPolySeed => _settingsStore.moneroSeedType == SeedType.polyseed; + bool get isPolySeed => _settingsStore.moneroSeedType == MoneroSeedType.polyseed; @action void setFiatApiMode(FiatApiMode fiatApiMode) => _settingsStore.fiatApiMode = fiatApiMode; diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index f5938911b..789d7da52 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -26,13 +27,13 @@ class WalletRestorationFromQRVM = WalletRestorationFromQRVMBase with _$WalletRes abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store { WalletRestorationFromQRVMBase(AppStore appStore, WalletCreationService walletCreationService, - Box walletInfoSource, WalletType type) + Box walletInfoSource, WalletType type, SeedSettingsViewModel seedSettingsViewModel) : height = 0, viewKey = '', spendKey = '', wif = '', address = '', - super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true); + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true); @observable int height; diff --git a/lib/view_model/seed_settings_view_model.dart b/lib/view_model/seed_settings_view_model.dart new file mode 100644 index 000000000..3a9536885 --- /dev/null +++ b/lib/view_model/seed_settings_view_model.dart @@ -0,0 +1,34 @@ +import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/seed_settings_store.dart'; +import 'package:mobx/mobx.dart'; + +part 'seed_settings_view_model.g.dart'; + +class SeedSettingsViewModel = SeedSettingsViewModelBase with _$SeedSettingsViewModel; + +abstract class SeedSettingsViewModelBase with Store { + SeedSettingsViewModelBase(this._appStore, this._seedSettingsStore); + + @computed + MoneroSeedType get moneroSeedType => _appStore.settingsStore.moneroSeedType; + + @action + void setMoneroSeedType(MoneroSeedType seedType) => _appStore.settingsStore.moneroSeedType = seedType; + + @computed + BitcoinSeedType get bitcoinSeedType => _appStore.settingsStore.bitcoinSeedType; + + @action + void setBitcoinSeedType(BitcoinSeedType derivationType) => + _appStore.settingsStore.bitcoinSeedType = derivationType; + + @computed + String? get passphrase => this._seedSettingsStore.passphrase; + + @action + void setPassphrase(String? passphrase) => this._seedSettingsStore.passphrase = passphrase; + + final AppStore _appStore; + final SeedSettingsStore _seedSettingsStore; +} diff --git a/lib/view_model/seed_type_view_model.dart b/lib/view_model/seed_type_view_model.dart deleted file mode 100644 index e3597be1c..000000000 --- a/lib/view_model/seed_type_view_model.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:cake_wallet/entities/seed_type.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:mobx/mobx.dart'; - -part 'seed_type_view_model.g.dart'; - -class SeedTypeViewModel = SeedTypeViewModelBase with _$SeedTypeViewModel; - -abstract class SeedTypeViewModelBase with Store { - SeedTypeViewModelBase(this._appStore); - - @computed - SeedType get moneroSeedType => _appStore.settingsStore.moneroSeedType; - - @action - void setMoneroSeedType(SeedType seedType) => _appStore.settingsStore.moneroSeedType = seedType; - - final AppStore _appStore; -} diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index e14934986..5760f5eec 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -1,20 +1,21 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; +import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; -import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; import 'package:polyseed/polyseed.dart'; part 'wallet_creation_vm.g.dart'; @@ -23,6 +24,7 @@ class WalletCreationVM = WalletCreationVMBase with _$WalletCreationVM; abstract class WalletCreationVMBase with Store { WalletCreationVMBase(this._appStore, this._walletInfoSource, this.walletCreationService, + this.seedSettingsViewModel, {required this.type, required this.isRecovery}) : state = InitialExecutionState(), name = ''; @@ -44,7 +46,6 @@ abstract class WalletCreationVMBase with Store { @observable String? repeatedWalletPassword; - bool get hasWalletPassword => SettingsStoreBase.walletPasswordDirectInput; WalletType type; @@ -52,6 +53,7 @@ abstract class WalletCreationVMBase with Store { final WalletCreationService walletCreationService; final Box _walletInfoSource; final AppStore _appStore; + final SeedSettingsViewModel seedSettingsViewModel; bool isPolyseed(String seed) => (type == WalletType.monero || type == WalletType.wownero) && @@ -109,17 +111,35 @@ abstract class WalletCreationVMBase with Store { getIt.get().registerSyncTask(); _appStore.authenticationStore.allowed(); state = ExecutedSuccessfullyState(); - } catch (e, s) { + } catch (e, _) { state = FailureState(e.toString()); } } DerivationInfo? getDefaultDerivation() { - switch (this.type) { + final useBip39 = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.bip39; + switch (type) { case WalletType.nano: return DerivationInfo(derivationType: DerivationType.nano); case WalletType.bitcoin: + if (useBip39) { + return DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/0'", + description: "Standard BIP84 native segwit", + scriptType: "p2wpkh", + ); + } + return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; case WalletType.litecoin: + if (useBip39) { + return DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/2'/0'", + description: "Default Litecoin", + scriptType: "p2wpkh", + ); + } return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; default: return null; diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 804ef7e3c..68bc95a00 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/wallet_base.dart'; @@ -25,10 +26,15 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with int _nextIndex = 0; - WalletHardwareRestoreViewModelBase(this.ledgerViewModel, AppStore appStore, - WalletCreationService walletCreationService, Box walletInfoSource, + WalletHardwareRestoreViewModelBase( + this.ledgerViewModel, + AppStore appStore, + WalletCreationService walletCreationService, + Box walletInfoSource, + SeedSettingsViewModel seedSettingsViewModel, {required WalletType type}) - : super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true); + : super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: true); @observable String name = ""; diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 1d5c27fed..9921ae30a 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -276,7 +276,8 @@ abstract class WalletKeysViewModelBase with Store { if (_appStore.wallet!.seed == null && _appStore.wallet!.privateKey != null) 'private_key': _appStore.wallet!.privateKey!, - if (restoreHeightResult != null) ...{'height': restoreHeightResult} + if (restoreHeightResult != null) ...{'height': restoreHeightResult}, + if (_appStore.wallet!.passphrase != null) 'passphrase': _appStore.wallet!.passphrase! }; } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index a618695b1..8c0f4f95f 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,35 +1,42 @@ -import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; -import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/wownero/wownero.dart'; -import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/wallet_base.dart'; -import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/haven/haven.dart'; -import 'advanced_privacy_settings_view_model.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; import '../polygon/polygon.dart'; +import 'advanced_privacy_settings_view_model.dart'; part 'wallet_new_vm.g.dart'; class WalletNewVM = WalletNewVMBase with _$WalletNewVM; abstract class WalletNewVMBase extends WalletCreationVM with Store { - WalletNewVMBase(AppStore appStore, WalletCreationService walletCreationService, - Box walletInfoSource, this.advancedPrivacySettingsViewModel, + WalletNewVMBase( + AppStore appStore, + WalletCreationService walletCreationService, + Box walletInfoSource, + this.advancedPrivacySettingsViewModel, + SeedSettingsViewModel seedSettingsViewModel, {required WalletType type}) : selectedMnemonicLanguage = '', - super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: false); + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: false); final AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel; @@ -37,47 +44,58 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { String selectedMnemonicLanguage; bool get hasLanguageSelector => - type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero; + [WalletType.monero, WalletType.haven, WalletType.wownero].contains(type); int get seedPhraseWordsLength { switch (type) { case WalletType.monero: case WalletType.wownero: - if (advancedPrivacySettingsViewModel.isPolySeed) { - return 16; - } - return 25; + return advancedPrivacySettingsViewModel.isPolySeed ? 16 : 25; case WalletType.tron: case WalletType.solana: case WalletType.polygon: case WalletType.ethereum: case WalletType.bitcoinCash: return advancedPrivacySettingsViewModel.seedPhraseLength.value; + case WalletType.bitcoin: + case WalletType.litecoin: + return seedSettingsViewModel.bitcoinSeedType == BitcoinSeedType.bip39 + ? advancedPrivacySettingsViewModel.seedPhraseLength.value + : 24; default: return 24; } } - bool get hasSeedType => type == WalletType.monero || type == WalletType.wownero; + bool get hasSeedType => [WalletType.monero, WalletType.wownero].contains(type); @override WalletCredentials getCredentials(dynamic _options) { final options = _options as List?; + final passphrase = seedSettingsViewModel.passphrase; + seedSettingsViewModel.setPassphrase(null); + switch (type) { case WalletType.monero: return monero!.createMoneroNewWalletCredentials( - name: name, language: options!.first as String, password: walletPassword, isPolyseed: options.last as bool); + name: name, + language: options!.first as String, + password: walletPassword, + isPolyseed: options.last as bool); case WalletType.bitcoin: - return bitcoin!.createBitcoinNewWalletCredentials(name: name, password: walletPassword); + return bitcoin!.createBitcoinNewWalletCredentials( + name: name, password: walletPassword, passphrase: passphrase); case WalletType.litecoin: - return bitcoin!.createBitcoinNewWalletCredentials(name: name, password: walletPassword); + return bitcoin!.createBitcoinNewWalletCredentials( + name: name, password: walletPassword, passphrase: passphrase); case WalletType.haven: return haven!.createHavenNewWalletCredentials( name: name, language: options!.first as String, password: walletPassword); case WalletType.ethereum: return ethereum!.createEthereumNewWalletCredentials(name: name, password: walletPassword); case WalletType.bitcoinCash: - return bitcoinCash!.createBitcoinCashNewWalletCredentials(name: name, password: walletPassword); + return bitcoinCash!.createBitcoinCashNewWalletCredentials( + name: name, password: walletPassword, passphrase: passphrase); case WalletType.nano: case WalletType.banano: return nano!.createNanoNewWalletCredentials(name: name); diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index a38baabd8..a365a2040 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -1,25 +1,26 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/nano/nano.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; -import 'package:cake_wallet/polygon/polygon.dart'; -import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/tron/tron.dart'; -import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; -import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; part 'wallet_restore_view_model.g.dart'; @@ -27,7 +28,7 @@ class WalletRestoreViewModel = WalletRestoreViewModelBase with _$WalletRestoreVi abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, - Box walletInfoSource, + Box walletInfoSource, SeedSettingsViewModel seedSettingsViewModel, {required WalletType type}) : hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, @@ -41,7 +42,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { type == WalletType.tron, isButtonEnabled = false, mode = WalletRestoreMode.seed, - super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) { + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true) { switch (type) { case WalletType.monero: availableModes = WalletRestoreMode.values; @@ -76,7 +77,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final bool hasBlockchainHeightLanguageSelector; final bool hasRestoreFromPrivateKey; - bool get hasPassphrase => [WalletType.bitcoin, WalletType.litecoin].contains(type); + bool get hasPassphrase => + [WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(type); @observable WalletRestoreMode mode; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index f84f6102b..89068d2d0 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "هل أنت متأكد أنك تريد حذف محفظة ${wallet_name}؟", "deleteConnectionConfirmationPrompt": "ـﺑ ﻝﺎﺼﺗﻻﺍ ﻑﺬﺣ ﺪﻳﺮﺗ ﻚﻧﺃ ﺪﻛﺄﺘﻣ ﺖﻧﺃ ﻞﻫ", "denominations": "الطوائف", + "derivationpath": "مسار الاشتقاق", "descending": "النزول", "description": "ﻒﺻﻭ", "destination_tag": "علامة الوجهة:", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 0b6b688bb..1ff8fdf85 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Сигурни ли сте, че искате да изтриете протфейла ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Сигурни ли сте, че искате да изтриете връзката към", "denominations": "Деноминации", + "derivationpath": "Пътят на производно", "descending": "Низходящ", "description": "Описание", "destination_tag": "Destination tag:", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index ef225041d..904c6d8e4 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Opravdu chcete smazat ${wallet_name} peněženku?", "deleteConnectionConfirmationPrompt": "Jste si jisti, že chcete smazat připojení k?", "denominations": "Označení", + "derivationpath": "Derivační cesta", "descending": "Klesající", "description": "Popis", "destination_tag": "Destination Tag:", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 393c3d928..860d4114d 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Sind Sie sicher, dass Sie das ${wallet_name} Wallet löschen möchten?", "deleteConnectionConfirmationPrompt": "Sind Sie sicher, dass Sie die Verbindung zu löschen möchten?", "denominations": "Konfessionen", + "derivationpath": "Ableitungspfad", "descending": "Absteigend", "description": "Beschreibung", "destination_tag": "Ziel-Tag:", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 4ac7dd8fd..9f89eb6ab 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Are you sure that you want to delete ${wallet_name} wallet?", "deleteConnectionConfirmationPrompt": "Are you sure that you want to delete the connection to", "denominations": "Denominations", + "derivationpath": "Derivation Path", "descending": "Descending", "description": "Description", "destination_tag": "Destination tag:", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c308f045a..250decfc4 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "¿Está seguro de que desea eliminar la billetera ${wallet_name}?", "deleteConnectionConfirmationPrompt": "¿Está seguro de que desea eliminar la conexión a", "denominations": "Denominaciones", + "derivationpath": "Ruta de derivación", "descending": "Descendente", "description": "Descripción", "destination_tag": "Etiqueta de destino:", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 4bc5c9809..297019aa0 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Êtes-vous sûr de vouloir supprimer le portefeuille (wallet) ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Êtes-vous sûr de vouloir supprimer la connexion à", "denominations": "Dénominations", + "derivationpath": "Chemin de dérivation", "descending": "Descendant", "description": "Description", "destination_tag": "Tag de destination :", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 025a33f6b..05cdf27d4 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Shin kun tabbata cewa kuna son share jakar ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Shin kun tabbata cewa kuna son share haɗin zuwa", "denominations": "Denominations", + "derivationpath": "Hanyar Nasara", "descending": "Saukowa", "description": "Bayani", "destination_tag": "Tambarin makoma:", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 346c420a9..30f86d9ae 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "क्या आप वाकई ${wallet_name} वॉलेट हटाना चाहते हैं?", "deleteConnectionConfirmationPrompt": "क्या आप वाकई कनेक्शन हटाना चाहते हैं?", "denominations": "मूल्यवर्ग", + "derivationpath": "व्युत्पत्ति पथ", "descending": "अवरोही", "description": "विवरण", "destination_tag": "गंतव्य टैग:", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 6f5bc88a6..0d6e9a0d8 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Jeste li sigurni da želite izbrisati ${wallet_name} novčanik?", "deleteConnectionConfirmationPrompt": "Jeste li sigurni da želite izbrisati vezu s", "denominations": "Denominacije", + "derivationpath": "Put derivacije", "descending": "Silazni", "description": "Opis", "destination_tag": "Odredišna oznaka:", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 2bc298aa7..65703e0bc 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Apakah Anda yakin ingin menghapus dompet ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Apakah Anda yakin ingin menghapus koneksi ke", "denominations": "Denominasi", + "derivationpath": "Jalur derivasi", "descending": "Menurun", "description": "Keterangan", "destination_tag": "Tag tujuan:", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 0548b0ad2..c2ba20ce0 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -196,6 +196,7 @@ "delete_wallet_confirm_message": "Sei sicuro di voler eliminare il portafoglio ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Sei sicuro di voler eliminare la connessione a", "denominations": "Denominazioni", + "derivationpath": "Percorso di derivazione", "descending": "Discendente", "description": "Descrizione", "destination_tag": "Tag destinazione:", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index c53a6d001..ab6605257 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "${wallet_name} ウォレットを削除してもよろしいですか?", "deleteConnectionConfirmationPrompt": "への接続を削除してもよろしいですか?", "denominations": "宗派", + "derivationpath": "派生パス", "descending": "下降", "description": "説明", "destination_tag": "宛先タグ:", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index be6757d99..da10162ad 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "${wallet_name} 지갑을 삭제하시겠습니까?", "deleteConnectionConfirmationPrompt": "다음 연결을 삭제하시겠습니까?", "denominations": "교파", + "derivationpath": "파생 경로", "descending": "내림차순", "description": "설명", "destination_tag": "목적지 태그:", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index c53dffb6b..e04c75d62 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "${wallet_name} ပိုက်ဆံအိတ်ကို ဖျက်လိုသည်မှာ သေချာပါသလား။", "deleteConnectionConfirmationPrompt": "ချိတ်ဆက်မှုကို ဖျက်လိုသည်မှာ သေချာပါသလား။", "denominations": "ဂိုဏ်းချုပ်ပစ္စည်းများ", + "derivationpath": "derivation လမ်းကြောင်း", "descending": "ဆင်း", "description": "ဖော်ပြချက်", "destination_tag": "ခရီးဆုံးအမှတ်-", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 2d55344f5..dfa7c2d8f 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Weet u zeker dat u de portemonnee van ${wallet_name} wilt verwijderen?", "deleteConnectionConfirmationPrompt": "Weet u zeker dat u de verbinding met", "denominations": "Denominaties", + "derivationpath": "Afleidingspad", "descending": "Aflopend", "description": "Beschrijving", "destination_tag": "Bestemmingstag:", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 833fc0308..397ea2bcb 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Czy na pewno chcesz usunąć portfel ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Czy na pewno chcesz usunąć połączenie z", "denominations": "Wyznaczenia", + "derivationpath": "Ścieżka pochodna", "descending": "Schodzenie", "description": "Opis", "destination_tag": "Tag docelowy:", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index bd6d9b506..cbe66715a 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Tem certeza de que deseja excluir a carteira ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Tem certeza de que deseja excluir a conexão com", "denominations": "Denominações", + "derivationpath": "Caminho de derivação", "descending": "descendente", "description": "Descrição", "destination_tag": "Tag de destino:", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index d43572351..9976de9f8 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Вы уверены, что хотите удалить кошелек ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Вы уверены, что хотите удалить подключение к", "denominations": "Деноминации", + "derivationpath": "Путь получения", "descending": "Нисходящий", "description": "Описание", "destination_tag": "Целевой тег:", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index d948ba6d6..e873fe5eb 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "คุณแน่ใจหรือว่าต้องการลบกระเป๋า${wallet_name}?", "deleteConnectionConfirmationPrompt": "คุณแน่ใจหรือไม่ว่าต้องการลบการเชื่อมต่อไปยัง", "denominations": "นิกาย", + "derivationpath": "เส้นทางที่ได้มา", "descending": "ลงมา", "description": "คำอธิบาย", "destination_tag": "แท็กปลายทาง:", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index a0ab030c8..c1bd2bae7 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Sigurado ka ba na gusto mong tanggalin ang iyong ${wallet_name} wallet?", "deleteConnectionConfirmationPrompt": "Sigurado ka bang gusto mong tanggalin ang koneksyon sa", "denominations": "Mga Denominasyon", + "derivationpath": "Landas ng derivation", "descending": "Pababang", "description": "Paglalarawan", "destination_tag": "Tag ng patutunguhan:", @@ -661,14 +662,14 @@ "show_details": "Ipakita ang mga detalye", "show_keys": "Ipakita ang mga seed/key", "show_market_place": "Ipakita ang Marketplace", + "show_seed": "Ipakita ang seed", "sign_message": "Mag -sign Message", + "sign_up": "Mag-sign Up", "sign_verify_message": "Mag -sign o i -verify ang mensahe", "sign_verify_message_sub": "Mag -sign o i -verify ang isang mensahe gamit ang iyong pribadong key", "sign_verify_title": "Mag -sign / Mag -verify", "signature": "Lagda", "signature_invalid_error": "Ang lagda ay hindi wasto para sa ibinigay na mensahe", - "show_seed": "Ipakita ang seed", - "sign_up": "Mag-sign Up", "signTransaction": "Mag-sign ang Transaksyon", "signup_for_card_accept_terms": "Mag-sign up para sa card at tanggapin ang mga tuntunin.", "silent_payments": "Tahimik na pagbabayad", @@ -827,9 +828,9 @@ "use_testnet": "Gumamit ng testnet", "value": "Halaga", "value_type": "Uri ng halaga", - "verify_message": "I -verify ang mensahe", "variable_pair_not_supported": "Ang variable na pares na ito ay hindi suportado sa mga napiling exchange", "verification": "Pag-verify", + "verify_message": "I -verify ang mensahe", "verify_with_2fa": "Mag-verify sa Cake 2FA", "version": "Bersyon ${currentVersion}", "view_all": "Tingnan lahat", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index e7cdc5b12..ff7d9a120 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "${wallet_name} isimli cüzdanını silmek istediğinden emin misin?", "deleteConnectionConfirmationPrompt": "Bağlantıyı silmek istediğinizden emin misiniz?", "denominations": "Mezhepler", + "derivationpath": "Türev yolu", "descending": "Azalan", "description": "Tanım", "destination_tag": "Hedef Etiketi:", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 91b5b5266..95a3a38d8 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Ви впевнені, що хочете видалити гаманець ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Ви впевнені, що хочете видалити з’єднання з", "denominations": "Конфесія", + "derivationpath": "Шлях виведення", "descending": "Низхідний", "description": "опис", "destination_tag": "Тег призначення:", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index e54a0db34..5be1cb7a0 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "کیا آپ واقعی ${wallet_name} والیٹ کو حذف کرنا چاہتے ہیں؟", "deleteConnectionConfirmationPrompt": "۔ﮟﯿﮨ ﮯﺘﮨﺎﭼ ﺎﻧﺮﮐ ﻑﺬﺣ ﻮﮐ ﻦﺸﮑﻨﮐ ﭖﺁ ﮧﮐ ﮯﮨ ﻦﯿﻘﯾ ﻮﮐ ﭖﺁ ﺎﯿﮐ", "denominations": "فرق", + "derivationpath": "مشتق راستہ", "descending": "اترتے ہوئے", "description": "ﻞﯿﺼﻔﺗ", "destination_tag": "منزل کا ٹیگ:", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 8f47d8543..81d784d7f 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "Ṣó dá ẹ lójú pé ẹ fẹ́ pa àpamọ́wọ́ ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Ṣe o da ọ loju pe o fẹ paarẹ asopọ si", "denominations": "Awọn ede", + "derivationpath": "Ọna Deriji", "descending": "Sọkalẹ", "description": "Apejuwe", "destination_tag": "Orúkọ tí ìbí tó a ránṣẹ́ sí:", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index c864a529b..876c3bbfb 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -195,6 +195,7 @@ "delete_wallet_confirm_message": "您确定要删除 ${wallet_name} 钱包吗?", "deleteConnectionConfirmationPrompt": "您确定要删除与", "denominations": "教派", + "derivationpath": "推导路径", "descending": "下降", "description": "描述", "destination_tag": "目标Tag:", diff --git a/tool/configure.dart b/tool/configure.dart index c9b6bbdda..b3194c4c8 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -151,7 +151,7 @@ abstract class Bitcoin { String? passphrase, }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); - WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); + WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase}); WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); List getWordList(); Map getWalletKeys(Object wallet); @@ -1021,10 +1021,10 @@ abstract class BitcoinCash { Box walletInfoSource, Box unspentCoinSource, bool isDirect); WalletCredentials createBitcoinCashNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password}); + {required String name, WalletInfo? walletInfo, String? password, String? passphrase}); WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}); + {required String name, required String mnemonic, required String password, String? passphrase}); TransactionPriority deserializeBitcoinCashTransactionPriority(int raw); From 518bfbe40d1a70f08e9c25b0cf957f2b30eda282 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 26 Aug 2024 20:07:25 +0300 Subject: [PATCH 060/142] fetch accurate fee rates from Cake's mempool api (#1649) * fetch accurate fee rates from mempool api * remove duplicate exception handler onError call [skip ci] * add the domain name --- cw_bitcoin/lib/electrum_wallet.dart | 69 ++++++++++++++++--- cw_bitcoin/pubspec.lock | 63 ++++++++++++++++- cw_bitcoin/pubspec.yaml | 1 + cw_monero/lib/monero_wallet_service.dart | 38 +--------- lib/entities/preferences_key.dart | 1 + lib/src/screens/settings/privacy_page.dart | 29 ++++++++ lib/store/settings_store.dart | 10 +++ lib/utils/exception_handler.dart | 7 ++ .../settings/privacy_settings_view_model.dart | 11 +++ res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 2 + res/values/strings_en.arb | 2 + res/values/strings_es.arb | 4 +- res/values/strings_fr.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_hy.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + 36 files changed, 239 insertions(+), 46 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 0ce4844c6..be4cee4d8 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -5,6 +5,7 @@ import 'dart:isolate'; import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; @@ -44,6 +45,7 @@ import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; import 'package:hex/hex.dart'; +import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -101,6 +103,8 @@ abstract class ElectrumWalletBase ); reaction((_) => syncStatus, _syncStatusReaction); + + sharedPrefs.complete(SharedPreferences.getInstance()); } static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, @@ -196,6 +200,13 @@ abstract class ElectrumWalletBase bool _isTryingToConnect = false; + Completer sharedPrefs = Completer(); + + Future checkIfMempoolAPIIsEnabled() async { + bool isMempoolAPIEnabled = (await sharedPrefs.future).getBool("use_mempool_fee_api") ?? true; + return isMempoolAPIEnabled; + } + @action Future setSilentPaymentsScanning(bool active) async { silentPaymentsScanningActive = active; @@ -448,6 +459,20 @@ abstract class ElectrumWalletBase @action Future updateFeeRates() async { + if (await checkIfMempoolAPIIsEnabled()) { + try { + final response = + await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended")); + + final result = json.decode(response.body) as Map; + final slowFee = result['economyFee']?.toInt() ?? 0; + final mediumFee = result['hourFee']?.toInt() ?? 0; + final fastFee = result['fastestFee']?.toInt() ?? 0; + _feeRates = [slowFee, mediumFee, fastFee]; + return; + } catch (_) {} + } + final feeRates = await electrumClient.feeRates(network: network); if (feeRates != [0, 0, 0]) { _feeRates = feeRates; @@ -1455,7 +1480,6 @@ abstract class ElectrumWalletBase // Create a list of available outputs final outputs = []; for (final out in bundle.originalTransaction.outputs) { - // Check if the script contains OP_RETURN final script = out.scriptPubKey.script; if (script.contains('OP_RETURN') && memo == null) { @@ -2058,13 +2082,42 @@ abstract class ElectrumWalletBase tx.inputAddresses!.isEmpty || tx.outputAddresses == null || tx.outputAddresses!.isEmpty) { - tx = ElectrumTransactionInfo.fromElectrumBundle( - bundle, - walletInfo.type, - network, - addresses: addressesSet, - height: tx.height, - ); + List inputAddresses = []; + List outputAddresses = []; + + for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) { + final input = bundle.originalTransaction.inputs[i]; + final inputTransaction = bundle.ins[i]; + final vout = input.txIndex; + final outTransaction = inputTransaction.outputs[vout]; + final address = addressFromOutputScript(outTransaction.scriptPubKey, network); + + if (address.isNotEmpty) inputAddresses.add(address); + } + + for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) { + final out = bundle.originalTransaction.outputs[i]; + final address = addressFromOutputScript(out.scriptPubKey, network); + + if (address.isNotEmpty) outputAddresses.add(address); + + // Check if the script contains OP_RETURN + final script = out.scriptPubKey.script; + if (script.contains('OP_RETURN')) { + final index = script.indexOf('OP_RETURN'); + if (index + 1 <= script.length) { + try { + final opReturnData = script[index + 1].toString(); + final decodedString = utf8.decode(HEX.decode(opReturnData)); + outputAddresses.add('OP_RETURN:$decodedString'); + } catch (_) { + outputAddresses.add('OP_RETURN:'); + } + } + } + } + tx.inputAddresses = inputAddresses; + tx.outputAddresses = outputAddresses; transactionHistory.addOne(tx); } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index cdb88946e..b5e59cd88 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -350,6 +350,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -736,6 +741,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -944,4 +1005,4 @@ packages: version: "2.2.1" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.6" + flutter: ">=3.19.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 2af1ac54e..84164b884 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 intl: ^0.18.0 + shared_preferences: ^2.0.15 cw_core: path: ../cw_core bitbox: diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index f9973f430..7f4672d43 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -9,11 +9,9 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; -import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/monero_wallet.dart'; -import 'package:flutter/widgets.dart'; import 'package:hive/hive.dart'; import 'package:polyseed/polyseed.dart'; import 'package:monero/monero.dart' as monero; @@ -120,7 +118,6 @@ class MoneroWalletService extends WalletService< @override Future openWallet(String name, String password, {bool? retryOnFailure}) async { - MoneroWallet? wallet; try { final path = await pathForWallet(name: name, type: getType()); @@ -147,41 +144,10 @@ class MoneroWalletService extends WalletService< await wallet.init(); return wallet; - } catch (e, s) { + } catch (e) { // TODO: Implement Exception for wallet list service. - final bool isBadAlloc = e.toString().contains('bad_alloc') || - (e is WalletOpeningException && - (e.message == 'std::bad_alloc' || e.message.contains('bad_alloc'))); - - final bool doesNotCorrespond = e.toString().contains('does not correspond') || - (e is WalletOpeningException && e.message.contains('does not correspond')); - - final bool isMissingCacheFilesIOS = e.toString().contains('basic_string') || - (e is WalletOpeningException && e.message.contains('basic_string')); - - final bool isMissingCacheFilesAndroid = e.toString().contains('input_stream') || - e.toString().contains('input stream error') || - (e is WalletOpeningException && - (e.message.contains('input_stream') || e.message.contains('input stream error'))); - - final bool invalidSignature = e.toString().contains('invalid signature') || - (e is WalletOpeningException && e.message.contains('invalid signature')); - - final bool invalidPassword = e.toString().contains('invalid password') || - (e is WalletOpeningException && e.message.contains('invalid password')); - - if (!isBadAlloc && - !doesNotCorrespond && - !isMissingCacheFilesIOS && - !isMissingCacheFilesAndroid && - !invalidSignature && - !invalidPassword && - wallet != null && - wallet.onError != null) { - wallet.onError!(FlutterErrorDetails(exception: e, stack: s)); - } - if (invalidPassword || retryOnFailure == false) { + if (retryOnFailure == false) { rethrow; } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 743357f92..714612ff1 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -61,6 +61,7 @@ class PreferencesKey { static const useEtherscan = 'use_etherscan'; static const usePolygonScan = 'use_polygonscan'; static const useTronGrid = 'use_trongrid'; + static const useMempoolFeeAPI = 'use_mempool_fee_api'; static const defaultNanoRep = 'default_nano_representative'; static const defaultBananoRep = 'default_banano_representative'; static const lookupsTwitter = 'looks_up_twitter'; diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 0eaf3ffbd..7db244460 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -6,7 +6,9 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:flutter/material.dart'; @@ -111,6 +113,33 @@ class PrivacyPage extends BasePage { _privacySettingsViewModel.setUseTronGrid(value); }, ), + if (_privacySettingsViewModel.canUseMempoolFeeAPI) + SettingsSwitcherCell( + title: S.current.live_fee_rates, + value: _privacySettingsViewModel.useMempoolFeeAPI, + onValueChange: (BuildContext _, bool isEnabled) async { + if (!isEnabled) { + final bool confirmation = await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).warning, + alertContent: S.of(context).disable_fee_api_warning, + rightButtonText: S.of(context).confirm, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; + if (confirmation) { + _privacySettingsViewModel.setUseMempoolFeeAPI(isEnabled); + } + return; + } + + _privacySettingsViewModel.setUseMempoolFeeAPI(isEnabled); + }, + ), SettingsCellWithArrow( title: S.current.domain_looks_up, handler: (context) => Navigator.of(context).pushNamed(Routes.domainLookupsPage), diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index ea5f43d98..f2738a293 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -101,6 +101,7 @@ abstract class SettingsStoreBase with Store { required this.useEtherscan, required this.usePolygonScan, required this.useTronGrid, + required this.useMempoolFeeAPI, required this.defaultNanoRep, required this.defaultBananoRep, required this.lookupsTwitter, @@ -416,6 +417,9 @@ abstract class SettingsStoreBase with Store { reaction((_) => useTronGrid, (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid)); + reaction((_) => useMempoolFeeAPI, + (bool useMempoolFeeAPI) => _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); + reaction((_) => defaultNanoRep, (String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep)); @@ -707,6 +711,9 @@ abstract class SettingsStoreBase with Store { @observable bool useTronGrid; + @observable + bool useMempoolFeeAPI; + @observable String defaultNanoRep; @@ -892,6 +899,7 @@ abstract class SettingsStoreBase with Store { final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; final usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; final useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; + final useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; final defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; final lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; @@ -1154,6 +1162,7 @@ abstract class SettingsStoreBase with Store { useEtherscan: useEtherscan, usePolygonScan: usePolygonScan, useTronGrid: useTronGrid, + useMempoolFeeAPI: useMempoolFeeAPI, defaultNanoRep: defaultNanoRep, defaultBananoRep: defaultBananoRep, lookupsTwitter: lookupsTwitter, @@ -1307,6 +1316,7 @@ abstract class SettingsStoreBase with Store { useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; + useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 5e0c83f88..8fc8a8c0e 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -174,6 +174,13 @@ class ExceptionHandler { "ClientException: Write failed, uri=http", "Connection terminated during handshake", "Corrupted wallets seeds", + "bad_alloc", + "does not correspond", + "basic_string", + "input_stream", + "input stream error", + "invalid signature", + "invalid password", ]; static Future _addDeviceInfo(File file) async { diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 90511af8e..c1e0fb1ce 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -75,6 +76,9 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get useTronGrid => _settingsStore.useTronGrid; + @computed + bool get useMempoolFeeAPI => _settingsStore.useMempoolFeeAPI; + @computed bool get lookupTwitter => _settingsStore.lookupsTwitter; @@ -99,6 +103,8 @@ abstract class PrivacySettingsViewModelBase with Store { bool get canUseTronGrid => _wallet.type == WalletType.tron; + bool get canUseMempoolFeeAPI => _wallet.type == WalletType.bitcoin; + @action void setShouldSaveRecipientAddress(bool value) => _settingsStore.shouldSaveRecipientAddress = value; @@ -156,4 +162,9 @@ abstract class PrivacySettingsViewModelBase with Store { _settingsStore.useTronGrid = value; tron!.updateTronGridUsageState(_wallet, value); } + + @action + void setUseMempoolFeeAPI(bool value) { + _settingsStore.useMempoolFeeAPI = value; + } } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 89068d2d0..0b238ad23 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -208,6 +208,7 @@ "disable_buy": "تعطيل إجراء الشراء", "disable_cake_2fa": "تعطيل 2 عامل المصادقة", "disable_exchange": "تعطيل التبادل", + "disable_fee_api_warning": "من خلال إيقاف تشغيل هذا ، قد تكون معدلات الرسوم غير دقيقة في بعض الحالات ، لذلك قد ينتهي بك الأمر إلى دفع مبالغ زائدة أو دفع رسوم المعاملات الخاصة بك", "disable_fiat": "تعطيل fiat", "disable_sell": "قم بتعطيل إجراء البيع", "disableBatteryOptimization": "تعطيل تحسين البطارية", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "يرجى التأكد", "ledger_please_enable_bluetooth": "يرجى تمكين البلوتوث للكشف عن دفتر الأستاذ الخاص بك", "light_theme": "فاتح", + "live_fee_rates": "أسعار الرسوم المباشرة عبر API", "load_more": "تحميل المزيد", "loading_your_wallet": "يتم تحميل محفظتك", "login": "تسجيل الدخول", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 1ff8fdf85..c13d6d7d6 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -208,6 +208,7 @@ "disable_buy": "Деактивирайте действието за покупка", "disable_cake_2fa": "Деактивирайте Cake 2FA", "disable_exchange": "Деактивиране на борса", + "disable_fee_api_warning": "Като изключите това, таксите могат да бъдат неточни в някои случаи, така че може да се препланите или да не плащате таксите за вашите транзакции", "disable_fiat": "Деактивиране на fiat", "disable_sell": "Деактивирайте действието за продажба", "disableBatteryOptimization": "Деактивирайте оптимизацията на батерията", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Моля, уверете се, че сте отворили правилното приложение на вашата книга", "ledger_please_enable_bluetooth": "Моля, активирайте Bluetooth да открие вашата книга", "light_theme": "Светло", + "live_fee_rates": "Цени на таксите на живо чрез API", "load_more": "Зареди още", "loading_your_wallet": "Зареждане на портфейл", "login": "Влизане", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 904c6d8e4..49bc856fd 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -208,6 +208,7 @@ "disable_buy": "Zakázat akci nákupu", "disable_cake_2fa": "Zakázat Cake 2FA", "disable_exchange": "Zakázat směnárny", + "disable_fee_api_warning": "Tímto vypnutím by sazby poplatků mohly být v některých případech nepřesné, takže byste mohli skončit přepláváním nebo nedoplatkem poplatků za vaše transakce", "disable_fiat": "Zakázat fiat", "disable_sell": "Zakázat akci prodeje", "disableBatteryOptimization": "Zakázat optimalizaci baterie", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Ujistěte se, že se na své knize otevřete správnou aplikaci", "ledger_please_enable_bluetooth": "Umožněte prosím Bluetooth detekovat vaši knihu", "light_theme": "Světlý", + "live_fee_rates": "Živé sazby poplatků prostřednictvím API", "load_more": "Načíst další", "loading_your_wallet": "Načítám peněženku", "login": "Login", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 860d4114d..19b7fbc90 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -208,6 +208,7 @@ "disable_buy": "Kaufaktion deaktivieren", "disable_cake_2fa": "Cake 2FA deaktivieren", "disable_exchange": "Exchange deaktivieren", + "disable_fee_api_warning": "Wenn dies ausgeschaltet wird, sind die Gebührenquoten in einigen Fällen möglicherweise ungenau, sodass Sie die Gebühren für Ihre Transaktionen möglicherweise überbezahlt oder unterzahlt", "disable_fiat": "Fiat deaktivieren", "disable_sell": "Verkaufsaktion deaktivieren", "disableBatteryOptimization": "Batterieoptimierung deaktivieren", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Bitte stellen Sie sicher, dass Sie die richtige App auf Ihrem Ledger geöffnet haben", "ledger_please_enable_bluetooth": "Bitte aktivieren Sie Bluetooth um sich mit Ihren Ledger zu verbinden.", "light_theme": "Hell", + "live_fee_rates": "Live -Gebührenpreise über API", "load_more": "Mehr laden", "loading_your_wallet": "Wallet wird geladen", "login": "Einloggen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 9f89eb6ab..75c0fc9c0 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -208,6 +208,7 @@ "disable_buy": "Disable buy action", "disable_cake_2fa": "Disable Cake 2FA", "disable_exchange": "Disable exchange", + "disable_fee_api_warning": "By turning this off, the fee rates might be inaccurate in some cases, so you might end up overpaying or underpaying the fees for your transactions", "disable_fiat": "Disable fiat", "disable_sell": "Disable sell action", "disableBatteryOptimization": "Disable Battery Optimization", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Please make sure you opend the right app on your ledger", "ledger_please_enable_bluetooth": "Please enable Bluetooth to detect your Ledger", "light_theme": "Light", + "live_fee_rates": "Live fee rates via API", "load_more": "Load more", "loading_your_wallet": "Loading your wallet", "login": "Login", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 250decfc4..73a2e77b9 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -208,6 +208,7 @@ "disable_buy": "Desactivar acción de compra", "disable_cake_2fa": "Desactivar pastel 2FA", "disable_exchange": "Deshabilitar intercambio", + "disable_fee_api_warning": "Al apagar esto, las tasas de tarifas pueden ser inexactas en algunos casos, por lo que puede terminar pagando en exceso o pagando menos las tarifas por sus transacciones", "disable_fiat": "Deshabilitar fiat", "disable_sell": "Desactivar acción de venta", "disableBatteryOptimization": "Deshabilitar la optimización de la batería", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Por favor, asegúrese de abrir la aplicación correcta en su libro mayor.", "ledger_please_enable_bluetooth": "Habilite Bluetooth para detectar su libro mayor", "light_theme": "Ligera", + "live_fee_rates": "Tasas de tarifas en vivo a través de API", "load_more": "Carga más", "loading_your_wallet": "Cargando tu billetera", "login": "Iniciar sesión", @@ -895,4 +897,4 @@ "you_will_get": "Convertir a", "you_will_send": "Convertir de", "yy": "YY" -} +} \ No newline at end of file diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 297019aa0..e1f557060 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -208,6 +208,7 @@ "disable_buy": "Désactiver l'action d'achat", "disable_cake_2fa": "Désactiver Cake 2FA", "disable_exchange": "Désactiver l'échange", + "disable_fee_api_warning": "En désactivant cela, les taux de frais peuvent être inexacts dans certains cas, vous pourriez donc finir par payer trop ou sous-paiement les frais pour vos transactions", "disable_fiat": "Désactiver les montants en fiat", "disable_sell": "Désactiver l'action de vente", "disableBatteryOptimization": "Désactiver l'optimisation de la batterie", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Veuillez vous assurer d'ouvrir la bonne application sur votre grand livre", "ledger_please_enable_bluetooth": "Veuillez activer Bluetooth pour détecter votre grand livre", "light_theme": "Clair", + "live_fee_rates": "Taux de frais en direct via l'API", "load_more": "Charger plus", "loading_your_wallet": "Chargement de votre portefeuille (wallet)", "login": "Utilisateur", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 05cdf27d4..4e0eb83f9 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -208,6 +208,7 @@ "disable_buy": "Kashe alama", "disable_cake_2fa": "Musaki Cake 2FA", "disable_exchange": "Kashe musanya", + "disable_fee_api_warning": "Ta hanyar juya wannan kashe, kudaden da zai iya zama ba daidai ba a wasu halaye, saboda haka zaku iya ƙare da overpaying ko a ƙarƙashin kudaden don ma'amaloli", "disable_fiat": "Dakatar da fiat", "disable_sell": "Kashe karbuwa", "disableBatteryOptimization": "Kashe ingantawa baturi", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Da fatan za a tabbata kun yi amfani da app ɗin dama akan dillalarku", "ledger_please_enable_bluetooth": "Da fatan za a kunna Bluetooth don gano Ledger ɗinku", "light_theme": "Haske", + "live_fee_rates": "Kudin Kiɗa ta API", "load_more": "Like more", "loading_your_wallet": "Ana loda walat ɗin ku", "login": "Shiga", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 30f86d9ae..f77547992 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -208,6 +208,7 @@ "disable_buy": "खरीद कार्रवाई अक्षम करें", "disable_cake_2fa": "केक 2FA अक्षम करें", "disable_exchange": "एक्सचेंज अक्षम करें", + "disable_fee_api_warning": "इसे बंद करने से, कुछ मामलों में शुल्क दरें गलत हो सकती हैं, इसलिए आप अपने लेनदेन के लिए फीस को कम कर सकते हैं या कम कर सकते हैं", "disable_fiat": "िएट को अक्षम करें", "disable_sell": "बेचने की कार्रवाई अक्षम करें", "disableBatteryOptimization": "बैटरी अनुकूलन अक्षम करें", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "कृपया सुनिश्चित करें कि आप अपने लेजर पर सही ऐप को खोलते हैं", "ledger_please_enable_bluetooth": "कृपया अपने बहीखाने का पता लगाने के लिए ब्लूटूथ को सक्षम करें", "light_theme": "रोशनी", + "live_fee_rates": "एपीआई के माध्यम से लाइव शुल्क दरें", "load_more": "और लोड करें", "loading_your_wallet": "अपना बटुआ लोड कर रहा है", "login": "लॉग इन करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 0d6e9a0d8..db02c5552 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -208,6 +208,7 @@ "disable_buy": "Onemogući kupnju", "disable_cake_2fa": "Onemogući Cake 2FA", "disable_exchange": "Onemogući exchange", + "disable_fee_api_warning": "Isključivanjem ovoga, stope naknade u nekim bi slučajevima mogle biti netočne, tako da biste mogli preplatiti ili predati naknadu za vaše transakcije", "disable_fiat": "Isključi, fiat", "disable_sell": "Onemogući akciju prodaje", "disableBatteryOptimization": "Onemogući optimizaciju baterije", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Obavezno obavezno otvorite pravu aplikaciju na knjizi", "ledger_please_enable_bluetooth": "Omogućite Bluetooth da otkrije svoju knjigu", "light_theme": "Svijetla", + "live_fee_rates": "Stope naknada uživo putem API -ja", "load_more": "Učitaj više", "loading_your_wallet": "Novčanik se učitava", "login": "Prijava", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 826bfa99a..178254bba 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -207,6 +207,7 @@ "disable_buy": "Անջատել գնում գործողությունը", "disable_cake_2fa": "Անջատել Cake 2FA", "disable_exchange": "Անջատել փոխանակումը", + "disable_fee_api_warning": "Դրանից անջատելով, վճարների տեմպերը որոշ դեպքերում կարող են անճիշտ լինել, այնպես որ դուք կարող եք վերջ տալ ձեր գործարքների համար վճարների գերավճարների կամ գերավճարների վրա", "disable_fiat": "Անջատել ֆիատ", "disable_sell": "Անջատել վաճառք գործողությունը", "disableBatteryOptimization": "Անջատել մարտկոցի օպտիմիզացիան", @@ -354,6 +355,7 @@ "ledger_error_wrong_app": "Խնդրում ենք համոզվել, որ դուք բացել եք ճիշտ ծրագիրը ձեր Ledger-ում", "ledger_please_enable_bluetooth": "Խնդրում ենք միացնել Bluetooth-ը ձեր Ledger-ը հայտնաբերելու համար", "light_theme": "Լուսավոր", + "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով", "load_more": "Բեռնել ավելին", "loading_your_wallet": "Ձեր հաշվեհամարը բեռնում է", "login": "Մուտք", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 65703e0bc..e67402de2 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -208,6 +208,7 @@ "disable_buy": "Nonaktifkan tindakan beli", "disable_cake_2fa": "Nonaktifkan Kue 2FA", "disable_exchange": "Nonaktifkan pertukaran", + "disable_fee_api_warning": "Dengan mematikan ini, tarif biaya mungkin tidak akurat dalam beberapa kasus, jadi Anda mungkin akan membayar lebih atau membayar biaya untuk transaksi Anda", "disable_fiat": "Nonaktifkan fiat", "disable_sell": "Nonaktifkan aksi jual", "disableBatteryOptimization": "Nonaktifkan optimasi baterai", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Pastikan Anda membuka aplikasi yang tepat di buku besar Anda", "ledger_please_enable_bluetooth": "Harap aktifkan Bluetooth untuk mendeteksi buku besar Anda", "light_theme": "Terang", + "live_fee_rates": "Tarif biaya langsung melalui API", "load_more": "Muat lebih banyak", "loading_your_wallet": "Memuat dompet Anda", "login": "Masuk", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index c2ba20ce0..a764520d8 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -209,6 +209,7 @@ "disable_buy": "Disabilita l'azione di acquisto", "disable_cake_2fa": "Disabilita Cake 2FA", "disable_exchange": "Disabilita scambio", + "disable_fee_api_warning": "Disattivando questo, i tassi delle commissioni potrebbero essere inaccurati in alcuni casi, quindi potresti finire in eccesso o sostenere le commissioni per le transazioni", "disable_fiat": "Disabilita fiat", "disable_sell": "Disabilita l'azione di vendita", "disableBatteryOptimization": "Disabilita l'ottimizzazione della batteria", @@ -356,6 +357,7 @@ "ledger_error_wrong_app": "Assicurati di aprire l'app giusta sul libro mastro", "ledger_please_enable_bluetooth": "Si prega di consentire al Bluetooth di rilevare il libro mastro", "light_theme": "Bianco", + "live_fee_rates": "Tariffe delle commissioni dal vivo tramite API", "load_more": "Carica di più", "loading_your_wallet": "Caricamento portafoglio", "login": "Accedi", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index ab6605257..bda62536e 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -208,6 +208,7 @@ "disable_buy": "購入アクションを無効にする", "disable_cake_2fa": "Cake 2FA を無効にする", "disable_exchange": "交換を無効にする", + "disable_fee_api_warning": "これをオフにすることで、料金金利は場合によっては不正確になる可能性があるため、取引の費用が過払いまたは不足している可能性があります", "disable_fiat": "フィアットを無効にする", "disable_sell": "販売アクションを無効にする", "disableBatteryOptimization": "バッテリーの最適化を無効にします", @@ -356,6 +357,7 @@ "ledger_error_wrong_app": "元帳に適切なアプリを開始するようにしてください", "ledger_please_enable_bluetooth": "Bluetoothが元帳を検出できるようにしてください", "light_theme": "光", + "live_fee_rates": "API経由のライブ料金", "load_more": "もっと読み込む", "loading_your_wallet": "ウォレットをロードしています", "login": "ログイン", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index da10162ad..df339df55 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -208,6 +208,7 @@ "disable_buy": "구매 행동 비활성화", "disable_cake_2fa": "케이크 2FA 비활성화", "disable_exchange": "교환 비활성화", + "disable_fee_api_warning": "이것을 끄면 경우에 따라 수수료가 부정확 할 수 있으므로 거래 수수료를 초과 지불하거나 지불 할 수 있습니다.", "disable_fiat": "법정화폐 비활성화", "disable_sell": "판매 조치 비활성화", "disableBatteryOptimization": "배터리 최적화를 비활성화합니다", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "원장에서 올바른 앱을 반대하는지 확인하십시오.", "ledger_please_enable_bluetooth": "Bluetooth가 원장을 감지 할 수 있도록하십시오", "light_theme": "빛", + "live_fee_rates": "API를 통한 라이브 요금 요금", "load_more": "더로드하십시오", "loading_your_wallet": "지갑 넣기", "login": "로그인", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index e04c75d62..e988c6c8d 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -208,6 +208,7 @@ "disable_buy": "ဝယ်ယူမှု လုပ်ဆောင်ချက်ကို ပိတ်ပါ။", "disable_cake_2fa": "ကိတ်မုန့် 2FA ကို ပိတ်ပါ။", "disable_exchange": "လဲလှယ်မှုကို ပိတ်ပါ။", + "disable_fee_api_warning": "ဤအရာကိုဖွင့်ခြင်းအားဖြင့်အချို့သောကိစ္စရပ်များတွင်အခကြေးငွေနှုန်းထားများသည်တိကျမှုရှိနိုင်သည်,", "disable_fiat": "Fiat ကိုပိတ်ပါ။", "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။", "disableBatteryOptimization": "ဘက်ထရီ optimization ကိုပိတ်ပါ", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "ကျေးဇူးပြု. သင့်လက်ျာအက်ပ်ကိုသင်၏ Ledger တွင်ဖွင့်ရန်သေချာစေပါ", "ledger_please_enable_bluetooth": "သင်၏ Ledger ကိုရှာဖွေရန် Bluetooth ကိုဖွင့်ပါ", "light_theme": "အလင်း", + "live_fee_rates": "API မှတစ်ဆင့် Live အခကြေးငွေနှုန်းထားများ", "load_more": "ပိုပြီး load", "loading_your_wallet": "သင့်ပိုက်ဆံအိတ်ကို ဖွင့်နေသည်။", "login": "လော့ဂ်အင်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index dfa7c2d8f..d1f6032e5 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -208,6 +208,7 @@ "disable_buy": "Koopactie uitschakelen", "disable_cake_2fa": "Taart 2FA uitschakelen", "disable_exchange": "Uitwisseling uitschakelen", + "disable_fee_api_warning": "Door dit uit te schakelen, kunnen de tarieven in sommige gevallen onnauwkeurig zijn, dus u kunt de vergoedingen voor uw transacties te veel betalen of te weinig betalen", "disable_fiat": "Schakel Fiat uit", "disable_sell": "Verkoopactie uitschakelen", "disableBatteryOptimization": "Schakel de batterijoptimalisatie uit", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Zorg ervoor dat u de juiste app op uw grootboek opent", "ledger_please_enable_bluetooth": "Schakel Bluetooth in staat om uw grootboek te detecteren", "light_theme": "Licht", + "live_fee_rates": "Live -tarieven via API", "load_more": "Meer laden", "loading_your_wallet": "Uw portemonnee laden", "login": "Log in", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 397ea2bcb..3b4e0f81f 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -208,6 +208,7 @@ "disable_buy": "Wyłącz akcję kupna", "disable_cake_2fa": "Wyłącz Cake 2FA", "disable_exchange": "Wyłącz wymianę", + "disable_fee_api_warning": "Wyłączając to, stawki opłaty mogą być w niektórych przypadkach niedokładne, więc możesz skończyć się przepłaceniem lub wynagrodzeniem opłat za transakcje", "disable_fiat": "Wyłącz waluty FIAT", "disable_sell": "Wyłącz akcję sprzedaży", "disableBatteryOptimization": "Wyłącz optymalizację baterii", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Upewnij się, że opisz odpowiednią aplikację na swojej księdze", "ledger_please_enable_bluetooth": "Włącz Bluetooth wykrywanie księgi", "light_theme": "Jasny", + "live_fee_rates": "Stawki opłaty na żywo za pośrednictwem API", "load_more": "Załaduj więcej", "loading_your_wallet": "Ładowanie portfela", "login": "Login", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index cbe66715a..6b9e4bf77 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -208,6 +208,7 @@ "disable_buy": "Desativar ação de compra", "disable_cake_2fa": "Desabilitar o Cake 2FA", "disable_exchange": "Desativar troca", + "disable_fee_api_warning": "Ao desativar isso, as taxas de taxas podem ser imprecisas em alguns casos, para que você possa acabar pagando demais ou pagando as taxas por suas transações", "disable_fiat": "Desativar fiat", "disable_sell": "Desativar ação de venda", "disableBatteryOptimization": "Desative a otimização da bateria", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Por favor, certifique -se de optar pelo aplicativo certo no seu livro", "ledger_please_enable_bluetooth": "Ative o Bluetooth para detectar seu livro", "light_theme": "Luz", + "live_fee_rates": "Taxas de taxas ao vivo via API", "load_more": "Carregue mais", "loading_your_wallet": "Abrindo sua carteira", "login": "Login", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 9976de9f8..a8def9179 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -208,6 +208,7 @@ "disable_buy": "Отключить действие покупки", "disable_cake_2fa": "Отключить торт 2FA", "disable_exchange": "Отключить обмен", + "disable_fee_api_warning": "Выключив это, в некоторых случаях ставки платы могут быть неточными, так что вы можете в конечном итоге переплачивать или недоплачивать сборы за ваши транзакции", "disable_fiat": "Отключить фиат", "disable_sell": "Отключить действие продажи", "disableBatteryOptimization": "Отключить оптимизацию батареи", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Пожалуйста, убедитесь, что вы предлагаете правильное приложение в своей бухгалтерской книге", "ledger_please_enable_bluetooth": "Пожалуйста, включите Bluetooth обнаружить вашу бухгалтерскую книгу", "light_theme": "Светлая", + "live_fee_rates": "Ставки по сбору вживую через API", "load_more": "Загрузи больше", "loading_your_wallet": "Загрузка кошелька", "login": "Логин", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index e873fe5eb..02480d6fd 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -208,6 +208,7 @@ "disable_buy": "ปิดการใช้งานการซื้อ", "disable_cake_2fa": "ปิดการใช้งานเค้ก 2FA", "disable_exchange": "ปิดใช้งานการแลกเปลี่ยน", + "disable_fee_api_warning": "โดยการปิดสิ่งนี้อัตราค่าธรรมเนียมอาจไม่ถูกต้องในบางกรณีดังนั้นคุณอาจจบลงด้วยการจ่ายเงินมากเกินไปหรือจ่ายค่าธรรมเนียมสำหรับการทำธุรกรรมของคุณมากเกินไป", "disable_fiat": "ปิดใช้งานสกุลเงินตรา", "disable_sell": "ปิดการใช้งานการขาย", "disableBatteryOptimization": "ปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "โปรดตรวจสอบให้แน่ใจว่าคุณเปิดแอพที่เหมาะสมในบัญชีแยกประเภทของคุณ", "ledger_please_enable_bluetooth": "โปรดเปิดใช้งานบลูทู ธ ในการตรวจจับบัญชีแยกประเภทของคุณ", "light_theme": "สว่าง", + "live_fee_rates": "อัตราค่าธรรมเนียมสดผ่าน API", "load_more": "โหลดมากขึ้น", "loading_your_wallet": "กำลังโหลดกระเป๋าของคุณ", "login": "เข้าสู่ระบบ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index c1bd2bae7..2307cc62b 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -208,6 +208,7 @@ "disable_buy": "Huwag paganahin ang pagkilos ng pagbili", "disable_cake_2fa": "Huwag paganahin ang Cake 2FA", "disable_exchange": "Huwag paganahin ang palitan", + "disable_fee_api_warning": "Sa pamamagitan ng pag -off nito, ang mga rate ng bayad ay maaaring hindi tumpak sa ilang mga kaso, kaya maaari mong tapusin ang labis na bayad o pagsuporta sa mga bayarin para sa iyong mga transaksyon", "disable_fiat": "Huwag paganahin ang fiat", "disable_sell": "Huwag paganahin ang pagkilos ng pagbebenta", "disableBatteryOptimization": "Huwag Paganahin ang Pag-optimize ng Baterya", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Mangyaring tiyaking pinipili mo ang tamang app sa iyong Ledger", "ledger_please_enable_bluetooth": "Mangyaring paganahin ang Bluetooth upang makita ang iyong Ledger", "light_theme": "Light", + "live_fee_rates": "Mga rate ng live na bayad sa pamamagitan ng API", "load_more": "Mag-load pa", "loading_your_wallet": "Naglo-load ng iyong wallet", "login": "Mag-login", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index ff7d9a120..8346703bc 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -208,6 +208,7 @@ "disable_buy": "Satın alma işlemini devre dışı bırak", "disable_cake_2fa": "Cake 2FA'yı Devre Dışı Bırak", "disable_exchange": "Borsayı devre dışı bırak", + "disable_fee_api_warning": "Bunu kapatarak, ücret oranları bazı durumlarda yanlış olabilir, bu nedenle işlemleriniz için ücretleri fazla ödeyebilir veya az ödeyebilirsiniz.", "disable_fiat": "İtibari paraları devre dışı bırak", "disable_sell": "Satış işlemini devre dışı bırak", "disableBatteryOptimization": "Pil optimizasyonunu devre dışı bırakın", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Lütfen defterinizde doğru uygulamayı açtığınızdan emin olun", "ledger_please_enable_bluetooth": "Defterinizi algılamak için lütfen Bluetooth'u etkinleştirin", "light_theme": "Aydınlık", + "live_fee_rates": "API üzerinden canlı ücret oranları", "load_more": "Daha fazla yükle", "loading_your_wallet": "Cüzdanın yükleniyor", "login": "Login", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 95a3a38d8..ec2324544 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -208,6 +208,7 @@ "disable_buy": "Вимкнути дію покупки", "disable_cake_2fa": "Вимкнути Cake 2FA", "disable_exchange": "Вимкнути exchange", + "disable_fee_api_warning": "Вимкнувши це, ставки плати в деяких випадках можуть бути неточними, тому ви можете переплатити або недооплатити плату за свої транзакції", "disable_fiat": "Вимкнути фиат", "disable_sell": "Вимкнути дію продажу", "disableBatteryOptimization": "Вимкнути оптимізацію акумулятора", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "Будь ласка, переконайтеся, що ви відкриваєте потрібну програму на своїй книзі", "ledger_please_enable_bluetooth": "Будь ласка, ввімкніть Bluetooth виявити свою книгу", "light_theme": "Світла", + "live_fee_rates": "Ставки плати за живий через API", "load_more": "Завантажити ще", "loading_your_wallet": "Завантаження гаманця", "login": "Логін", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 5be1cb7a0..bc5fc332c 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -208,6 +208,7 @@ "disable_buy": "خرید ایکشن کو غیر فعال کریں۔", "disable_cake_2fa": "کیک 2FA کو غیر فعال کریں۔", "disable_exchange": "تبادلے کو غیر فعال کریں۔", + "disable_fee_api_warning": "اس کو بند کرنے سے ، کچھ معاملات میں فیس کی شرح غلط ہوسکتی ہے ، لہذا آپ اپنے لین دین کے لئے فیسوں کو زیادہ ادائیگی یا ادائیگی ختم کرسکتے ہیں۔", "disable_fiat": "فیاٹ کو غیر فعال کریں۔", "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔", "disableBatteryOptimization": "بیٹری کی اصلاح کو غیر فعال کریں", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "براہ کرم یقینی بنائیں کہ آپ اپنے لیجر پر صحیح ایپ کو کھولتے ہیں", "ledger_please_enable_bluetooth": "براہ کرم بلوٹوتھ کو اپنے لیجر کا پتہ لگانے کے لئے اہل بنائیں", "light_theme": "روشنی", + "live_fee_rates": "API کے ذریعے براہ راست فیس کی شرح", "load_more": "مزید لوڈ کریں", "loading_your_wallet": "آپ کا بٹوہ لوڈ ہو رہا ہے۔", "login": "لاگ ان کریں", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 81d784d7f..e13d79b6b 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -208,6 +208,7 @@ "disable_buy": "Ko iṣọrọ ọja", "disable_cake_2fa": "Ko 2FA Cake sii", "disable_exchange": "Pa ilé pàṣípààrọ̀", + "disable_fee_api_warning": "Nipa yiyi eyi kuro, awọn oṣuwọn owo naa le jẹ aiṣe deede ni awọn ọrọ kan, nitorinaa o le pari apọju tabi awọn idiyele ti o ni agbara fun awọn iṣowo rẹ", "disable_fiat": "Pa owó tí ìjọba pàṣẹ wa lò", "disable_sell": "Ko iṣọrọ iṣọrọ", "disableBatteryOptimization": "Mu Ifasi batiri", @@ -356,6 +357,7 @@ "ledger_error_wrong_app": "Jọwọ rii daju pe iwọ yoo sọ app ti o tọ loju omi rẹ", "ledger_please_enable_bluetooth": "Jọwọ jẹ ki Bluetooth lati rii iṣupọ rẹ", "light_theme": "Funfun bí eérú", + "live_fee_rates": "Awọn oṣuwọn Owo laaye laaye nipasẹ API", "load_more": "Ẹru diẹ sii", "loading_your_wallet": "A ń ṣí àpamọ́wọ́ yín", "login": "Orúkọ", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 876c3bbfb..a26edec4e 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -208,6 +208,7 @@ "disable_buy": "禁用购买操作", "disable_cake_2fa": "禁用蛋糕 2FA", "disable_exchange": "禁用交换", + "disable_fee_api_warning": "通过将其关闭,在某些情况下,收费率可能不准确,因此您最终可能会超额付款或支付交易费用", "disable_fiat": "禁用法令", "disable_sell": "禁用卖出操作", "disableBatteryOptimization": "禁用电池优化", @@ -355,6 +356,7 @@ "ledger_error_wrong_app": "请确保您在分类帐中操作正确的应用程序", "ledger_please_enable_bluetooth": "请启用蓝牙来检测您的分类帐", "light_theme": "艳丽", + "live_fee_rates": "通过API的实时费率", "load_more": "装载更多", "loading_your_wallet": "加载您的钱包", "login": "登录", From 4c795ea5c26f2b5ba88317cf6d02fd284bc420fb Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 27 Aug 2024 01:40:20 +0200 Subject: [PATCH 061/142] subaddress fix (#1620) * subaddress fix * fix subaddress generation * rewrite usedAddresses for xmr and wow * [skip ci] remove print statements --- cw_monero/lib/api/transaction_history.dart | 27 ++++++++++++++++---- cw_monero/lib/monero_subaddress_list.dart | 9 ++++--- cw_monero/lib/monero_wallet.dart | 5 ++++ cw_monero/lib/monero_wallet_addresses.dart | 20 +++++++++++++++ cw_wownero/lib/api/transaction_history.dart | 23 +++++++++++++---- cw_wownero/lib/wownero_subaddress_list.dart | 4 +++ cw_wownero/lib/wownero_wallet.dart | 6 +++++ cw_wownero/lib/wownero_wallet_addresses.dart | 18 +++++++++++++ 8 files changed, 99 insertions(+), 13 deletions(-) diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index b416e1b4e..1f00b082f 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -45,6 +45,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -245,19 +247,30 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = monero.Wallet_address( + String get subaddressLabel => monero.Wallet_getSubaddressLabel( wptr!, - accountIndex: 0, - addressIndex: 0, + accountIndex: accountIndex, + addressIndex: addressIndex, ); + String get address => monero.Wallet_address( + wptr!, + accountIndex: accountIndex, + addressIndex: addressIndex, + ); + List get addressList => List.generate(addressIndexList.length, (index) => + monero.Wallet_address( + wptr!, + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 10; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; @@ -303,6 +316,8 @@ class Transaction { 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), @@ -316,6 +331,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndexList, + required this.addressIndex, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index c35afb282..fe85bef3b 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_monero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -103,6 +104,9 @@ abstract class MoneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -124,7 +128,7 @@ abstract class MoneroSubaddressListBase with Store { Future> _getAllUnusedAddresses( {required int accountIndex, required String label}) async { final allAddresses = subaddress_list.getAllSubaddresses(); - if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last)) { + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.first.address)) { final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); if (!isAddressUnused) { return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); @@ -143,8 +147,7 @@ abstract class MoneroSubaddressListBase with Store { label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' : label); - }) - .toList(); + }).toList().reversed.toList(); } Future _newSubaddress({required int accountIndex, required String label}) async { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index f5fa0ec7e..483ee8868 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -88,6 +88,9 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -130,6 +133,7 @@ abstract class MoneroWalletBase extends WalletBase get usedAddresses { + final txs = getAllTransactions(); + final adds = _originalUsedAddresses.toList(); + for (var i = 0; i < txs.length; i++) { + for (var j = 0; j < txs[i].addressList.length; j++) { + adds.add(txs[i].addressList[j]); + } + } + return adds.toSet(); + } + + Set _originalUsedAddresses = Set(); + + @override + set usedAddresses(Set _usedAddresses) { + _originalUsedAddresses = _usedAddresses; + } + @override Future init() async { accountList.update(); diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart index a1e1e3c9b..020c47df6 100644 --- a/cw_wownero/lib/api/transaction_history.dart +++ b/cw_wownero/lib/api/transaction_history.dart @@ -45,6 +45,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -243,19 +245,26 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = wownero.Wallet_address( + String get subaddressLabel => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); + String get address => wownero.Wallet_address( wptr!, - accountIndex: 0, - addressIndex: 0, + accountIndex: accountIndex, + addressIndex: addressIndex, ); + List get addressList => List.generate(addressIndexList.length, (index) => + wownero.Wallet_address( + wptr!, + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 3; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; @@ -301,6 +310,8 @@ class Transaction { amount = wownero.TransactionInfo_amount(txInfo), paymentId = wownero.TransactionInfo_paymentId(txInfo), accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo), + addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, + addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), blockheight = wownero.TransactionInfo_blockHeight(txInfo), confirmations = wownero.TransactionInfo_confirmations(txInfo), fee = wownero.TransactionInfo_fee(txInfo), @@ -314,6 +325,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndex, + required this.addressIndexList, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_wownero/lib/wownero_subaddress_list.dart b/cw_wownero/lib/wownero_subaddress_list.dart index 61fd09ef9..5c026cc86 100644 --- a/cw_wownero/lib/wownero_subaddress_list.dart +++ b/cw_wownero/lib/wownero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_wownero/api/coins_info.dart'; import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_wownero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -103,6 +104,9 @@ abstract class WowneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index c3f4bcb69..ab7691dd6 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -82,6 +82,10 @@ abstract class WowneroWalletBase reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -123,6 +127,7 @@ abstract class WowneroWalletBase wownero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; + ReactionDisposer? _onTxHistoryChangeReaction; bool _isTransactionUpdating; bool _hasSyncAfterStartup; Timer? _autoSaveTimer; @@ -158,6 +163,7 @@ abstract class WowneroWalletBase void close() async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); + _onTxHistoryChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); } diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index 9eeb182eb..b36c0e9ec 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -3,6 +3,7 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/wownero_account_list.dart'; import 'package:cw_wownero/wownero_subaddress_list.dart'; @@ -36,7 +37,24 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; + @override + Set get usedAddresses { + final txs = getAllTransactions(); + final adds = _originalUsedAddresses.toList(); + for (var i = 0; i < txs.length; i++) { + for (var j = 0; j < txs[i].addressList.length; j++) { + adds.add(txs[i].addressList[j]); + } + } + return adds.toSet(); + } + Set _originalUsedAddresses = Set(); + + @override + set usedAddresses(Set _usedAddresses) { + _originalUsedAddresses = _usedAddresses; + } @override Future init() async { accountList.update(); From f072e79bb6ca3f0c9ef65607509d4839d1c4d832 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 27 Aug 2024 20:49:12 +0300 Subject: [PATCH 062/142] V4.19.5 v1.16.5 (#1654) * some not enough enhancements for sync status * update app versions * minor fix --- assets/text/Monerocom_Release_Notes.txt | 5 ++--- assets/text/Release_Notes.txt | 7 ++++--- cw_bitcoin/lib/electrum.dart | 12 +----------- cw_bitcoin/lib/electrum_wallet.dart | 10 ++++++---- cw_monero/lib/monero_wallet.dart | 4 +--- lib/reactions/check_connection.dart | 4 +++- lib/utils/exception_handler.dart | 1 - scripts/android/app_env.sh | 8 ++++---- scripts/ios/app_env.sh | 8 ++++---- scripts/linux/app_env.sh | 4 ++-- scripts/macos/app_env.sh | 8 ++++---- scripts/windows/build_exe_installer.iss | 2 +- tool/configure.dart | 2 +- 13 files changed, 33 insertions(+), 42 deletions(-) diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 11a3ad803..fec1485ac 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,2 @@ -Scan and verify messages -Synchronization enhancements -Bug fixes \ No newline at end of file +Enhance auto-address generation for Monero +Bug fixes and enhancements \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 11a3ad803..4e8a79fc1 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,3 +1,4 @@ -Scan and verify messages -Synchronization enhancements -Bug fixes \ No newline at end of file +Enable BIP39 by default for wallet creation also on Bitcoin/Litecoin (Electrum seed type is still accessible through advanced settings page) +Improve fee calculation for Bitcoin to protect against overpaying or underpaying +Enhance auto-address generation for Monero +Bug fixes and enhancements \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 6ac8edd1a..bc630a4e1 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -107,16 +107,9 @@ class ElectrumClient { } }, onError: (Object error) { - socket = null; final errorMsg = error.toString(); print(errorMsg); unterminatedString = ''; - - final currentHost = socket?.address.host; - final isErrorForCurrentHost = errorMsg.contains(" ${currentHost} "); - - if (currentHost != null && isErrorForCurrentHost) - _setConnectionStatus(ConnectionStatus.failed); }, onDone: () { unterminatedString = ''; @@ -436,7 +429,6 @@ class ElectrumClient { {required String id, required String method, List params = const []}) { try { if (socket == null) { - _setConnectionStatus(ConnectionStatus.failed); return null; } final subscription = BehaviorSubject(); @@ -453,7 +445,6 @@ class ElectrumClient { Future call( {required String method, List params = const [], Function(int)? idCallback}) async { if (socket == null) { - _setConnectionStatus(ConnectionStatus.failed); return null; } final completer = Completer(); @@ -467,10 +458,9 @@ class ElectrumClient { } Future callWithTimeout( - {required String method, List params = const [], int timeout = 4000}) async { + {required String method, List params = const [], int timeout = 5000}) async { try { if (socket == null) { - _setConnectionStatus(ConnectionStatus.failed); return null; } final completer = Completer(); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index be4cee4d8..73ae7c38f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -276,7 +276,6 @@ abstract class ElectrumWalletBase Future? _isolate; void Function(FlutterErrorDetails)? _onError; - Timer? _reconnectTimer; Timer? _autoSaveTimer; Timer? _updateFeeRateTimer; static const int _autoSaveInterval = 1; @@ -429,6 +428,10 @@ abstract class ElectrumWalletBase @override Future startSync() async { try { + if (syncStatus is SyncronizingSyncStatus) { + return; + } + syncStatus = SyncronizingSyncStatus(); if (hasSilentPaymentsScanning) { @@ -2055,9 +2058,8 @@ abstract class ElectrumWalletBase _isTryingToConnect = true; - _reconnectTimer?.cancel(); - _reconnectTimer = Timer(Duration(seconds: 10), () { - if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) { + Timer(Duration(seconds: 10), () { + if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) { this.electrumClient.connectToUri( node!.uri, useSSL: node!.useSSL ?? false, diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 483ee8868..8dc3fdf86 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -13,11 +13,9 @@ import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/monero_wallet_keys.dart'; import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; @@ -88,7 +86,7 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); - reaction((_) => transactionHistory, (__) { + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); }); } diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index 3252797dd..812fa9fcd 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -24,7 +24,9 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore return; } - if (wallet.syncStatus is LostConnectionSyncStatus || wallet.syncStatus is FailedSyncStatus) { + if (wallet.type != WalletType.bitcoin && + (wallet.syncStatus is LostConnectionSyncStatus || + wallet.syncStatus is FailedSyncStatus)) { final alive = await settingsStore.getCurrentNode(wallet.type).requestNode(); if (alive) { diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 8fc8a8c0e..dd58d3e56 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -172,7 +172,6 @@ class ExceptionHandler { "Error while launching http", "OS Error: Network is unreachable", "ClientException: Write failed, uri=http", - "Connection terminated during handshake", "Corrupted wallets seeds", "bad_alloc", "does not correspond", diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 324d2d2ae..59919dcf8 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.4" -MONERO_COM_BUILD_NUMBER=98 +MONERO_COM_VERSION="1.16.5" +MONERO_COM_BUILD_NUMBER=99 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.4" -CAKEWALLET_BUILD_NUMBER=225 +CAKEWALLET_VERSION="4.19.5" +CAKEWALLET_BUILD_NUMBER=226 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 1405f7939..70530f4e6 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.4" -MONERO_COM_BUILD_NUMBER=96 +MONERO_COM_VERSION="1.16.5" +MONERO_COM_BUILD_NUMBER=97 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.4" -CAKEWALLET_BUILD_NUMBER=263 +CAKEWALLET_VERSION="4.19.5" +CAKEWALLET_BUILD_NUMBER=264 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index bc2965193..7f1b11f3f 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.9.4" -CAKEWALLET_BUILD_NUMBER=31 +CAKEWALLET_VERSION="1.9.5" +CAKEWALLET_BUILD_NUMBER=32 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 342831d65..165feafe1 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.6.4" -MONERO_COM_BUILD_NUMBER=28 +MONERO_COM_VERSION="1.6.5" +MONERO_COM_BUILD_NUMBER=30 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.12.4" -CAKEWALLET_BUILD_NUMBER=85 +CAKEWALLET_VERSION="1.12.5" +CAKEWALLET_BUILD_NUMBER=86 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index 4d7838723..bb13f49ef 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.0.5" +#define MyAppVersion "0.0.6" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" diff --git a/tool/configure.dart b/tool/configure.dart index b3194c4c8..1d2166ed6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -79,7 +79,6 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; -import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/output_info.dart'; @@ -102,6 +101,7 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; From 82f64a42642516d1abed58c12be4018129c6e813 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Thu, 29 Aug 2024 00:52:11 +0300 Subject: [PATCH 063/142] revert subaddress fix (#1656) add show/hide passphrase --- cw_monero/lib/api/transaction_history.dart | 27 ++-------- cw_monero/lib/monero_subaddress_list.dart | 9 ++-- cw_monero/lib/monero_wallet.dart | 5 -- cw_monero/lib/monero_wallet_addresses.dart | 20 -------- cw_wownero/lib/api/transaction_history.dart | 23 ++------- cw_wownero/lib/wownero_subaddress_list.dart | 4 -- cw_wownero/lib/wownero_wallet.dart | 6 --- cw_wownero/lib/wownero_wallet_addresses.dart | 18 ------- .../advanced_privacy_settings_page.dart | 51 +++++++++++-------- scripts/android/app_env.sh | 4 +- scripts/ios/app_env.sh | 4 +- scripts/linux/app_env.sh | 2 +- scripts/macos/app_env.sh | 4 +- 13 files changed, 51 insertions(+), 126 deletions(-) diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 1f00b082f..b416e1b4e 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -45,8 +45,6 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, - addressIndex: 0, - addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -247,30 +245,19 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String get subaddressLabel => monero.Wallet_getSubaddressLabel( + String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); + late final String address = monero.Wallet_address( wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, + accountIndex: 0, + addressIndex: 0, ); - String get address => monero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, - ); - List get addressList => List.generate(addressIndexList.length, (index) => - monero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndexList[index], - )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 10; final int blockheight; - final int addressIndex; + final int addressIndex = 0; final int accountIndex; - final List addressIndexList; final String paymentId; final int amount; final bool isSpend; @@ -316,8 +303,6 @@ class Transaction { 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), @@ -331,8 +316,6 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, - required this.addressIndexList, - required this.addressIndex, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index fe85bef3b..c35afb282 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,7 +1,6 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; -import 'package:cw_monero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -104,9 +103,6 @@ abstract class MoneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); - final _all = _usedAddresses.toSet().toList(); - _usedAddresses.clear(); - _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -128,7 +124,7 @@ abstract class MoneroSubaddressListBase with Store { Future> _getAllUnusedAddresses( {required int accountIndex, required String label}) async { final allAddresses = subaddress_list.getAllSubaddresses(); - if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.first.address)) { + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last)) { final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); if (!isAddressUnused) { return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); @@ -147,7 +143,8 @@ abstract class MoneroSubaddressListBase with Store { label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' : label); - }).toList().reversed.toList(); + }) + .toList(); } Future _newSubaddress({required int accountIndex, required String label}) async { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 8dc3fdf86..8773d694d 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -86,9 +86,6 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); - _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { - _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); - }); } static const int _autoSaveInterval = 30; @@ -131,7 +128,6 @@ abstract class MoneroWalletBase extends WalletBase get usedAddresses { - final txs = getAllTransactions(); - final adds = _originalUsedAddresses.toList(); - for (var i = 0; i < txs.length; i++) { - for (var j = 0; j < txs[i].addressList.length; j++) { - adds.add(txs[i].addressList[j]); - } - } - return adds.toSet(); - } - - Set _originalUsedAddresses = Set(); - - @override - set usedAddresses(Set _usedAddresses) { - _originalUsedAddresses = _usedAddresses; - } - @override Future init() async { accountList.update(); diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart index 020c47df6..a1e1e3c9b 100644 --- a/cw_wownero/lib/api/transaction_history.dart +++ b/cw_wownero/lib/api/transaction_history.dart @@ -45,8 +45,6 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, - addressIndex: 0, - addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -245,26 +243,19 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String get subaddressLabel => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - String get address => wownero.Wallet_address( + String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); + late final String address = wownero.Wallet_address( wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, + accountIndex: 0, + addressIndex: 0, ); - List get addressList => List.generate(addressIndexList.length, (index) => - wownero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndexList[index], - )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 3; final int blockheight; - final int addressIndex; + final int addressIndex = 0; final int accountIndex; - final List addressIndexList; final String paymentId; final int amount; final bool isSpend; @@ -310,8 +301,6 @@ class Transaction { amount = wownero.TransactionInfo_amount(txInfo), paymentId = wownero.TransactionInfo_paymentId(txInfo), accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo), - addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, - addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), blockheight = wownero.TransactionInfo_blockHeight(txInfo), confirmations = wownero.TransactionInfo_confirmations(txInfo), fee = wownero.TransactionInfo_fee(txInfo), @@ -325,8 +314,6 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, - required this.addressIndex, - required this.addressIndexList, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_wownero/lib/wownero_subaddress_list.dart b/cw_wownero/lib/wownero_subaddress_list.dart index 5c026cc86..61fd09ef9 100644 --- a/cw_wownero/lib/wownero_subaddress_list.dart +++ b/cw_wownero/lib/wownero_subaddress_list.dart @@ -1,7 +1,6 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_wownero/api/coins_info.dart'; import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; -import 'package:cw_wownero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -104,9 +103,6 @@ abstract class WowneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); - final _all = _usedAddresses.toSet().toList(); - _usedAddresses.clear(); - _usedAddresses.addAll(_all); if (_isUpdating) { return; } diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index ab7691dd6..c3f4bcb69 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -82,10 +82,6 @@ abstract class WowneroWalletBase reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); - - _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { - _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); - }); } static const int _autoSaveInterval = 30; @@ -127,7 +123,6 @@ abstract class WowneroWalletBase wownero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; - ReactionDisposer? _onTxHistoryChangeReaction; bool _isTransactionUpdating; bool _hasSyncAfterStartup; Timer? _autoSaveTimer; @@ -163,7 +158,6 @@ abstract class WowneroWalletBase void close() async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); - _onTxHistoryChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); } diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index b36c0e9ec..9eeb182eb 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -3,7 +3,6 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/wownero_account_list.dart'; import 'package:cw_wownero/wownero_subaddress_list.dart'; @@ -37,24 +36,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; - @override - Set get usedAddresses { - final txs = getAllTransactions(); - final adds = _originalUsedAddresses.toList(); - for (var i = 0; i < txs.length; i++) { - for (var j = 0; j < txs[i].addressList.length; j++) { - adds.add(txs[i].addressList[j]); - } - } - return adds.toSet(); - } - Set _originalUsedAddresses = Set(); - - @override - set usedAddresses(Set _usedAddresses) { - _originalUsedAddresses = _usedAddresses; - } @override Future init() async { accountList.update(); 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 ff8ec3dd2..7173778cc 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -70,6 +70,8 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo final _formKey = GlobalKey(); bool? testnetValue; + bool obscurePassphrase = true; + @override void initState() { passphraseController.text = widget.seedTypeViewModel.passphrase ?? ''; @@ -138,27 +140,36 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ); }), if (!widget.isFromRestore) ...[ - Observer(builder: (_) { - if (widget.privacySettingsViewModel.hasSeedPhraseLengthOption) - return SettingsPickerCell( - title: S.current.seed_phrase_length, - items: SeedPhraseLength.values, - selectedItem: widget.privacySettingsViewModel.seedPhraseLength, - onItemSelected: (SeedPhraseLength length) { - widget.privacySettingsViewModel.setSeedPhraseLength(length); - }, - ); - return Container(); - }), - if (widget.privacySettingsViewModel.hasPassphraseOption) - Padding( - padding: EdgeInsets.all(24), - child: BaseTextFormField( - hintText: S.current.passphrase, - controller: passphraseController, - obscureText: true, + Observer(builder: (_) { + if (widget.privacySettingsViewModel.hasSeedPhraseLengthOption) + return SettingsPickerCell( + title: S.current.seed_phrase_length, + items: SeedPhraseLength.values, + selectedItem: widget.privacySettingsViewModel.seedPhraseLength, + onItemSelected: (SeedPhraseLength length) { + widget.privacySettingsViewModel.setSeedPhraseLength(length); + }, + ); + return Container(); + }), + if (widget.privacySettingsViewModel.hasPassphraseOption) + Padding( + padding: EdgeInsets.all(24), + child: BaseTextFormField( + hintText: S.current.passphrase, + controller: passphraseController, + obscureText: obscurePassphrase, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, + ), + ), + ), ), - ), ], Observer(builder: (_) { return Column( diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 59919dcf8..a60077e82 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -16,14 +16,14 @@ APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.16.5" -MONERO_COM_BUILD_NUMBER=99 +MONERO_COM_BUILD_NUMBER=100 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.19.5" -CAKEWALLET_BUILD_NUMBER=226 +CAKEWALLET_BUILD_NUMBER=227 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 70530f4e6..9df4b685e 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -14,12 +14,12 @@ APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.16.5" -MONERO_COM_BUILD_NUMBER=97 +MONERO_COM_BUILD_NUMBER=98 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.19.5" -CAKEWALLET_BUILD_NUMBER=264 +CAKEWALLET_BUILD_NUMBER=266 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 7f1b11f3f..1c89c6e8a 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -15,7 +15,7 @@ fi CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.9.5" -CAKEWALLET_BUILD_NUMBER=32 +CAKEWALLET_BUILD_NUMBER=33 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 165feafe1..a6afac2cb 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="1.6.5" -MONERO_COM_BUILD_NUMBER=30 +MONERO_COM_BUILD_NUMBER=31 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.12.5" -CAKEWALLET_BUILD_NUMBER=86 +CAKEWALLET_BUILD_NUMBER=88 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From c17f167266185b57822444752817f9ec38adfffb Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 29 Aug 2024 14:24:24 +0200 Subject: [PATCH 064/142] Fix windows build (#1658) --- windows/CMakeLists.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 7d1c03451..09bc8cfb6 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -87,18 +87,12 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x8 install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/wownero/x86_64-w64-mingw32_libwallet2_api_c.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "wownero_libwallet2_api_c.dll" COMPONENT Runtime) -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libgcc_s_seh-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libgcc_s_seh-1.dll" - COMPONENT Runtime) - install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libpolyseed.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libpolyseed.dll" COMPONENT Runtime) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libssp-0.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libssp-0.dll" COMPONENT Runtime) -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libstdc++-6.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libstdc++-6.dll" - COMPONENT Runtime) - install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libwinpthread-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libwinpthread-1.dll" COMPONENT Runtime) From 0c1ca78d298709a86bf3d67bc99d0e87ee3c7935 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Tue, 3 Sep 2024 20:34:26 -0700 Subject: [PATCH 065/142] allow restoring electrum seeds (#1665) --- .../screens/restore/wallet_restore_page.dart | 20 ++++++------------- .../restore/restore_from_qr_vm.dart | 2 +- lib/view_model/wallet_creation_vm.dart | 11 ++++++++-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 741befbc0..7d7fdb889 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -406,24 +406,16 @@ class WalletRestorePage extends BasePage { ) as DerivationInfo?; } else if (derivationsWithHistory == 1) { dInfo = derivations[derivationWithHistoryIndex]; - } - - // get the default derivation for this wallet type: - if (dInfo == null) { + } else if (derivations.length == 1) { // we only return 1 derivation if we're pretty sure we know which one to use: - if (derivations.length == 1) { - dInfo = derivations.first; - } else { - // if we have multiple possible derivations, and none have histories - // we just default to the most common one: - dInfo = walletRestoreViewModel.getCommonRestoreDerivation(); - } + dInfo = derivations.first; + } else { + // if we have multiple possible derivations, and none (or multiple) have histories + // we just default to the most common one: + dInfo = walletRestoreViewModel.getCommonRestoreDerivation(); } this.derivationInfo = dInfo; - if (this.derivationInfo == null) { - this.derivationInfo = walletRestoreViewModel.getDefaultDerivation(); - } await walletRestoreViewModel.create(options: _credentials()); seedSettingsViewModel.setPassphrase(null); diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 789d7da52..bb7d0f8d1 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -62,7 +62,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store derivationInfo = options["derivationInfo"] as DerivationInfo?; passphrase = options["passphrase"] as String?; } - derivationInfo ??= getDefaultDerivation(); + derivationInfo ??= getDefaultCreateDerivation(); switch (restoreWallet.restoreMode) { case WalletRestoreMode.keys: diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 5760f5eec..e563be37a 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -97,7 +97,7 @@ abstract class WalletCreationVMBase with Store { dirPath: dirPath, address: '', showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven, - derivationInfo: credentials.derivationInfo ?? getDefaultDerivation(), + derivationInfo: credentials.derivationInfo ?? getDefaultCreateDerivation(), hardwareWalletType: credentials.hardwareWalletType, ); @@ -116,7 +116,7 @@ abstract class WalletCreationVMBase with Store { } } - DerivationInfo? getDefaultDerivation() { + DerivationInfo? getDefaultCreateDerivation() { final useBip39 = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.bip39; switch (type) { case WalletType.nano: @@ -147,10 +147,14 @@ abstract class WalletCreationVMBase with Store { } DerivationInfo? getCommonRestoreDerivation() { + final useElectrum = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.electrum; switch (this.type) { case WalletType.nano: return DerivationInfo(derivationType: DerivationType.nano); case WalletType.bitcoin: + if (useElectrum) { + return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; + } return DerivationInfo( derivationType: DerivationType.bip39, derivationPath: "m/84'/0'/0'/0", @@ -158,6 +162,9 @@ abstract class WalletCreationVMBase with Store { scriptType: "p2wpkh", ); case WalletType.litecoin: + if (useElectrum) { + return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; + } return DerivationInfo( derivationType: DerivationType.bip39, derivationPath: "m/84'/2'/0'/0", From e29e5d9f3c6040f616c08e993aa349d64766c874 Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 5 Sep 2024 02:15:54 +0200 Subject: [PATCH 066/142] show pending status for outgoing transactions (#1669) --- lib/view_model/dashboard/transaction_list_item.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 176b4e58d..3101807b7 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -69,12 +69,10 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedStatus { - if (transaction.direction == TransactionDirection.incoming) { - if (balanceViewModel.wallet.type == WalletType.monero || - balanceViewModel.wallet.type == WalletType.wownero || - balanceViewModel.wallet.type == WalletType.haven) { - return formattedPendingStatus; - } + if (balanceViewModel.wallet.type == WalletType.monero || + balanceViewModel.wallet.type == WalletType.wownero || + balanceViewModel.wallet.type == WalletType.haven) { + return formattedPendingStatus; } return transaction.isPending ? S.current.pending : ''; } From 576dd49766cfd8c2aaaeec6803a4e35986c618e3 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 5 Sep 2024 03:54:06 +0300 Subject: [PATCH 067/142] Update check_box_picker.dart (#1660) --- lib/src/widgets/check_box_picker.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/src/widgets/check_box_picker.dart b/lib/src/widgets/check_box_picker.dart index b4db82628..847b5b1c4 100644 --- a/lib/src/widgets/check_box_picker.dart +++ b/lib/src/widgets/check_box_picker.dart @@ -113,10 +113,6 @@ class CheckBoxPickerState extends State { return GestureDetector( onTap: () { - if (item.isDisabled) { - return; - } - bool newValue = !item.value; item.value = newValue; widget.onChanged(index, newValue); @@ -134,7 +130,7 @@ class CheckBoxPickerState extends State { borderColor: Theme.of(context).dividerColor, iconColor: Colors.white, onChanged: (bool? value) { - if (value == null || item.isDisabled) { + if (value == null) { return; } From 783f1a234936dd75989956596159dd8a16a53b6f Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Wed, 4 Sep 2024 19:41:52 -0700 Subject: [PATCH 068/142] Nano rep page + derivation fixes (#1655) * minor nano derivation fixes * rep page fixes * ignore non-critical receive block errors * really be sure the derivation type is set during wallet creation --- cw_nano/lib/nano_client.dart | 32 ++++++---- cw_nano/lib/nano_wallet_service.dart | 31 +++++++-- .../screens/nano/nano_change_rep_page.dart | 64 +++++++++---------- lib/view_model/wallet_restore_view_model.dart | 12 ++-- 4 files changed, 80 insertions(+), 59 deletions(-) diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index 478a6c125..7f8e1d0a9 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -466,21 +466,25 @@ class NanoClient { blocks = blocks as Map; - // confirm all receivable blocks: - for (final blockHash in blocks.keys) { - final block = blocks[blockHash]; - final String amountRaw = block["amount"] as String; - await receiveBlock( - blockHash: blockHash, - amountRaw: amountRaw, - privateKey: privateKey, - destinationAddress: destinationAddress, - ); - // a bit of a hack: - await Future.delayed(const Duration(seconds: 2)); + try { + // confirm all receivable blocks: + for (final blockHash in blocks.keys) { + final block = blocks[blockHash]; + final String amountRaw = block["amount"] as String; + await receiveBlock( + blockHash: blockHash, + amountRaw: amountRaw, + privateKey: privateKey, + destinationAddress: destinationAddress, + ); + // a bit of a hack: + await Future.delayed(const Duration(seconds: 2)); + } + return blocks.keys.length; + } catch (_) { + // we failed to confirm all receivable blocks for w/e reason (PoW / node outage / etc) + return 0; } - - return blocks.keys.length; } void stop() {} diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index ac3d6581a..6dbcc68ab 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -14,8 +14,11 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:nanodart/nanodart.dart'; import 'package:nanoutil/nanoutil.dart'; -class NanoWalletService extends WalletService { +class NanoWalletService extends WalletService< + NanoNewWalletCredentials, + NanoRestoreWalletFromSeedCredentials, + NanoRestoreWalletFromKeysCredentials, + NanoNewWalletCredentials> { NanoWalletService(this.walletInfoSource, this.isDirect); final Box walletInfoSource; @@ -33,8 +36,12 @@ class NanoWalletService extends WalletService restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { + Future restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials, + {bool? isTestnet}) async { if (credentials.seedKey.contains(' ')) { throw Exception("Invalid key!"); } else { @@ -106,6 +114,13 @@ class NanoWalletService extends WalletService restoreFromHardwareWallet(NanoNewWalletCredentials credentials) { - throw UnimplementedError("Restoring a Nano wallet from a hardware wallet is not yet supported!"); + throw UnimplementedError( + "Restoring a Nano wallet from a hardware wallet is not yet supported!"); } @override - Future restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { + Future restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { if (credentials.mnemonic.contains(' ')) { if (!bip39.validateMnemonic(credentials.mnemonic)) { throw nm.NanoMnemonicIsIncorrectException(); diff --git a/lib/src/screens/nano/nano_change_rep_page.dart b/lib/src/screens/nano/nano_change_rep_page.dart index 9f71bb59c..60207b507 100644 --- a/lib/src/screens/nano/nano_change_rep_page.dart +++ b/lib/src/screens/nano/nano_change_rep_page.dart @@ -40,13 +40,11 @@ class NanoChangeRepPage extends BasePage { (node) => node.account == currentRepAccount, orElse: () => N2Node( account: currentRepAccount, - alias: currentRepAccount, score: 0, uptime: "???", weight: 0, ), ); - return currentNode; } @@ -57,9 +55,7 @@ class NanoChangeRepPage extends BasePage { child: FutureBuilder( future: nano!.getN2Reps(_wallet), builder: (context, snapshot) { - if (snapshot.data == null) { - return SizedBox(); - } + final reps = snapshot.data ?? []; return Container( padding: EdgeInsets.only(left: 24, right: 24), @@ -101,29 +97,35 @@ class NanoChangeRepPage extends BasePage { ), _buildSingleRepresentative( context, - getCurrentRepNode(snapshot.data as List), + getCurrentRepNode(reps), isList: false, + divider: false, ), - Divider(height: 20), - Container( - margin: EdgeInsets.only(top: 12), - child: Text( - S.current.nano_pick_new_rep, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, + if (reps.isNotEmpty) ...[ + Divider(height: 20), + Container( + margin: EdgeInsets.only(top: 12), + child: Text( + S.current.nano_pick_new_rep, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + ), ), ), - ), + Divider(height: 20), + ], ], ), ], ), contentPadding: EdgeInsets.only(bottom: 24), content: Container( - child: Column( - children: _getRepresentativeWidgets(context, snapshot.data as List), - ), + child: reps.isNotEmpty + ? Column( + children: _getRepresentativeWidgets(context, reps), + ) + : SizedBox(), ), bottomSectionPadding: EdgeInsets.only(bottom: 24), bottomSection: Observer( @@ -207,19 +209,22 @@ class NanoChangeRepPage extends BasePage { final List ret = []; for (final N2Node node in list) { if (node.alias != null && node.alias!.trim().isNotEmpty) { - ret.add(_buildSingleRepresentative(context, node)); + bool divider = node != list.first; + ret.add(_buildSingleRepresentative(context, node, divider: divider, isList: true)); } } return ret; } - Widget _buildSingleRepresentative(BuildContext context, N2Node rep, {bool isList = true}) { + Widget _buildSingleRepresentative( + BuildContext context, + N2Node rep, { + bool isList = true, + bool divider = false, + }) { return Column( children: [ - if (isList) - Divider( - height: 2, - ), + if (divider) Divider(height: 2), TextButton( style: TextButton.styleFrom( padding: EdgeInsets.zero, @@ -244,11 +249,11 @@ class NanoChangeRepPage extends BasePage { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _sanitizeAlias(rep.alias), + rep.alias ?? rep.account!, style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontWeight: FontWeight.w700, - fontSize: 18, + fontSize: rep.alias == null ? 14 : 18, ), ), Container( @@ -337,11 +342,4 @@ class NanoChangeRepPage extends BasePage { ], ); } - - String _sanitizeAlias(String? alias) { - if (alias != null) { - return alias.replaceAll(RegExp(r'[^a-zA-Z_.!?_;:-]'), ''); - } - return ''; - } } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index a365a2040..5462ce4c8 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -42,7 +42,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { type == WalletType.tron, isButtonEnabled = false, mode = WalletRestoreMode.seed, - super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true) { + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: true) { switch (type) { case WalletType.monero: availableModes = WalletRestoreMode.values; @@ -194,10 +195,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.nano: return nano!.createNanoRestoreWalletFromKeysCredentials( - name: name, - password: password, - seedKey: options['private_key'] as String, - derivationType: options["derivationType"] as DerivationType); + name: name, + password: password, + seedKey: options['private_key'] as String, + derivationType: derivationInfo!.derivationType!, + ); case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromPrivateKey( name: name, From 0b06ad3a0700296c626bbadb91db81f789be2a95 Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 5 Sep 2024 04:54:34 +0200 Subject: [PATCH 069/142] CW-702: fix mismatched amounts in multDest transactions (#1653) * CW-702: fix mismatched amounts in multDest transactions * separate txkeys in multdest transactions * update monero_c dependency --- cw_monero/lib/api/transaction_history.dart | 39 ++++++++++++++++++++-- cw_monero/pubspec.lock | 4 +-- cw_monero/pubspec.yaml | 2 +- cw_wownero/pubspec.lock | 4 +-- cw_wownero/pubspec.yaml | 2 +- lib/src/screens/send/send_page.dart | 3 ++ scripts/prepare_moneroc.sh | 2 +- 7 files changed, 46 insertions(+), 10 deletions(-) diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index b416e1b4e..7723b6051 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -138,11 +138,17 @@ PendingTransactionDescription createTransactionMultDestSync( int accountIndex = 0, List preferredInputs = const []}) { + final dstAddrs = outputs.map((e) => e.address).toList(); + final amounts = outputs.map((e) => monero.Wallet_amountFromString(e.amount)).toList(); + + // print("multDest: dstAddrs: $dstAddrs"); + // print("multDest: amounts: $amounts"); + final txptr = monero.Wallet_createTransactionMultDest( wptr!, - dstAddr: outputs.map((e) => e.address).toList(), + dstAddr: dstAddrs, isSweepAll: false, - amounts: outputs.map((e) => monero.Wallet_amountFromString(e.amount)).toList(), + amounts: amounts, mixinCount: 0, pendingTransactionPriority: priorityRaw, subaddr_account: accountIndex, @@ -307,7 +313,34 @@ class Transaction { confirmations = monero.TransactionInfo_confirmations(txInfo), fee = monero.TransactionInfo_fee(txInfo), description = monero.TransactionInfo_description(txInfo), - key = monero.Wallet_getTxKey(wptr!, txid: monero.TransactionInfo_hash(txInfo)); + key = getTxKey(txInfo); + + static String getTxKey(monero.TransactionInfo txInfo) { + final txKey = monero.Wallet_getTxKey(wptr!, txid: monero.TransactionInfo_hash(txInfo)); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + return monero.Wallet_errorString(wptr!); + } + return breakTxKey(txKey); + } + + static String breakTxKey(String input) { + final x = 64; + StringBuffer buffer = StringBuffer(); + + for (int i = 0; i < input.length; i += x) { + int endIndex = i + x; + if (endIndex > input.length) { + endIndex = input.length; + } + buffer.write(input.substring(i, endIndex)); + if (endIndex != input.length) { + buffer.write('\n\n'); + } + } + + return buffer.toString().trim(); + } Transaction.dummy({ required this.displayLabel, diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 13c70cfad..cf9da9455 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b - resolved-ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b + ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 + resolved-ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 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 3053977de..75d8b1ccc 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b # monero_c hash + ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 58831085f..b68e9ce74 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b - resolved-ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b + ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 + resolved-ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 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 b9306711d..39a2cc9c0 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: bcb328a4956105dc182afd0ce2e48fe263f5f20b # monero_c hash + ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 97a7ad88d..be2e6cb02 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -273,6 +273,7 @@ class SendPage extends BasePage { ? template.cryptoCurrency : template.fiatCurrency, onTap: () async { + sendViewModel.state = IsExecutingState(); if (template.additionalRecipients?.isNotEmpty ?? false) { sendViewModel.clearOutputs(); @@ -301,6 +302,7 @@ class SendPage extends BasePage { template: template, ); } + sendViewModel.state = InitialExecutionState(); }, onRemove: () { showPopUp( @@ -368,6 +370,7 @@ class SendPage extends BasePage { builder: (_) { return LoadingPrimaryButton( onPressed: () async { + if (sendViewModel.state is IsExecutingState) return; if (_formKey.currentState != null && !_formKey.currentState!.validate()) { if (sendViewModel.outputs.length > 1) { showErrorValidationAlert(context); diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 94754c935..1315a5825 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,7 +8,7 @@ if [[ ! -d "monero_c" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch rewrite-wip cd monero_c - git checkout 5de323b1ba7387cf73973042f06383d4dbe619f5 + git checkout 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero From 3869a71bd1306e6b8e2f7c06c44d01e7c0a48c3f Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 5 Sep 2024 03:59:20 +0100 Subject: [PATCH 070/142] CW-695: Fix Zone Mismatch Error Triggered By Restoring Backup (#1651) * Fix: Zone mismatch error triggered by restoring backup * fix: Adjust check for reinitializing --- lib/main.dart | 81 ++++++++++++------- .../restore_from_backup_view_model.dart | 2 +- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index aeb76b3a8..86a278fdc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,11 +48,14 @@ final rootKey = GlobalKey(); final RouteObserver> routeObserver = RouteObserver>(); Future main() async { + await runAppWithZone(); +} +Future runAppWithZone() async { bool isAppRunning = false; + await runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); - FlutterError.onError = ExceptionHandler.onError; /// A callback that is invoked when an unhandled error occurs in the root @@ -62,42 +65,14 @@ Future main() async { return true; }; - - await setDefaultMinimumWindowSize(); - - await CakeHive.close(); - - await initializeAppConfigs(); + await initializeAppAtRoot(); runApp(App()); - isAppRunning = true; }, (error, stackTrace) async { if (!isAppRunning) { runApp( - MaterialApp( - debugShowCheckedModeBanner: false, - scrollBehavior: AppScrollBehavior(), - home: Scaffold( - body: SingleChildScrollView( - child: Container( - margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), - child: Column( - children: [ - Text( - 'Error:\n${error.toString()}', - style: TextStyle(fontSize: 22), - ), - Text( - 'Stack trace:\n${stackTrace.toString()}', - style: TextStyle(fontSize: 16), - ), - ], - ), - ), - ), - ), - ), + TopLevelErrorWidget(error: error, stackTrace: stackTrace), ); } @@ -105,6 +80,12 @@ Future main() async { }); } +Future initializeAppAtRoot({bool reInitializing = false}) async { + if (!reInitializing) await setDefaultMinimumWindowSize(); + await CakeHive.close(); + await initializeAppConfigs(); +} + Future initializeAppConfigs() async { setRootDirFromEnv(); final appDir = await getAppDir(); @@ -338,3 +319,41 @@ class _HomeState extends State<_Home> { return const SizedBox.shrink(); } } + +class TopLevelErrorWidget extends StatelessWidget { + const TopLevelErrorWidget({ + required this.error, + required this.stackTrace, + super.key, + }); + + final Object error; + final StackTrace stackTrace; + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + scrollBehavior: AppScrollBehavior(), + home: Scaffold( + body: SingleChildScrollView( + child: Container( + margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + child: Column( + children: [ + Text( + 'Error:\n${error.toString()}', + style: TextStyle(fontSize: 22), + ), + Text( + 'Stack trace:\n${stackTrace.toString()}', + style: TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/view_model/restore_from_backup_view_model.dart b/lib/view_model/restore_from_backup_view_model.dart index 365afb1fa..432cac67e 100644 --- a/lib/view_model/restore_from_backup_view_model.dart +++ b/lib/view_model/restore_from_backup_view_model.dart @@ -46,7 +46,7 @@ abstract class RestoreFromBackupViewModelBase with Store { final data = await file.readAsBytes(); await backupService.importBackup(data, password); - await main(); + await initializeAppAtRoot(reInitializing: true); final store = getIt.get(); ReactionDisposer? reaction; From 580bd013457e40719f07ff22838467c02a1562d8 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Thu, 5 Sep 2024 10:32:31 -0400 Subject: [PATCH 071/142] Remove node.moneroworld.com (#1667) * Remove node.moneroworld.com from default nodes and apply settings migration * Small fix --- assets/node_list.yml | 3 -- lib/entities/default_settings_migration.dart | 33 ++++++++++++++------ lib/main.dart | 2 +- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/assets/node_list.yml b/assets/node_list.yml index bc7a9dc4a..d04a9a2e8 100644 --- a/assets/node_list.yml +++ b/assets/node_list.yml @@ -17,6 +17,3 @@ - uri: node.community.rino.io:18081 is_default: false -- - uri: node.moneroworld.com:18089 - is_default: false diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 3aad38179..802ad0184 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -40,6 +40,7 @@ const solanaDefaultNodeUri = 'rpc.ankr.com'; const tronDefaultNodeUri = 'trx.nownodes.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; +const moneroWorldNodeUri = '.moneroworld.com'; Future defaultSettingsMigration( {required int version, @@ -245,6 +246,9 @@ Future defaultSettingsMigration( _fixNodesUseSSLFlag(nodes); await changeDefaultNanoNode(nodes, sharedPreferences); break; + case 40: + await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); + break; default: break; } @@ -488,15 +492,7 @@ Node? getBitcoinCashDefaultElectrumServer({required Box nodes}) { Node getMoneroDefaultNode({required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; - var nodeUri = ''; - - if (timeZone >= 1) { - // Eurasia - nodeUri = 'xmr-node-eu.cakewallet.com:18081'; - } else if (timeZone <= -4) { - // America - nodeUri = 'xmr-node-usa-east.cakewallet.com:18081'; - } + var nodeUri = newCakeWalletMoneroUri; try { return nodes.values.firstWhere((Node node) => node.uriRaw == nodeUri); @@ -1260,3 +1256,22 @@ Future replaceTronDefaultNode({ // If it's not, we switch user to the new default node: NowNodes await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); } + +Future removeMoneroWorld( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + const cakeWalletMoneroNodeUriPattern = '.moneroworld.com'; + final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); + final currentMoneroNode = nodes.values.firstWhere((node) => node.key == currentMoneroNodeId); + final needToReplaceCurrentMoneroNode = currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); + + nodes.values.forEach((node) async { + if (node.type == WalletType.monero && + node.uri.toString().contains(cakeWalletMoneroNodeUriPattern)) { + await node.delete(); + } + }); + + if (needToReplaceCurrentMoneroNode) { + await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + } +} diff --git a/lib/main.dart b/lib/main.dart index 86a278fdc..cae528210 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -186,7 +186,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 39, + initialMigrationVersion: 40, ); } From f279a222df152c2e03c66fe910a36e6f49360ff5 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 6 Sep 2024 16:03:18 +0300 Subject: [PATCH 072/142] Cw 682 integrate stealth ex exchange provider (#1575) * add stealthEx provider * minor fix * Update pr_test_build.yml * Update dashboard_view_model.dart * update api key * add api key * add secret to linux [skip ci] * fix network param issue * additional fee percent [skip ci] * fix for poly network * add StealthEx tracking link. * minor fix * update name [skip ci] --------- Co-authored-by: OmarHatem --- .github/workflows/pr_test_build_android.yml | 2 + .github/workflows/pr_test_build_linux.yml | 2 + assets/images/stealthex.png | Bin 0 -> 7602 bytes .../exchange_provider_description.dart | 4 + .../stealth_ex_exchange_provider.dart | 299 ++++++++++++++++++ lib/exchange/trade_state.dart | 2 +- lib/store/dashboard/trade_filter_store.dart | 30 +- .../dashboard/dashboard_view_model.dart | 13 +- .../exchange/exchange_trade_view_model.dart | 3 + .../exchange/exchange_view_model.dart | 20 +- lib/view_model/trade_details_view_model.dart | 6 + tool/utils/secret_key.dart | 2 + 12 files changed, 362 insertions(+), 21 deletions(-) create mode 100644 assets/images/stealthex.png create mode 100644 lib/exchange/provider/stealth_ex_exchange_provider.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index b7b2aaa71..b60ae1f7e 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -168,6 +168,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart + echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart - name: Rename app run: | diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 7935dd177..e25bea2ec 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -154,6 +154,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart + echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart - name: Rename app run: | diff --git a/assets/images/stealthex.png b/assets/images/stealthex.png new file mode 100644 index 0000000000000000000000000000000000000000..311d47b74b1755f4490cae29c77387cdd5808e3d GIT binary patch literal 7602 zcmeHM{XbOc-@ndrVicJ{8InOHr3_LknJLOsq1aMuGm@}vQ7S5#v#avZXw+709!A@f zo|Y}GRLqo&L@1R?p*?xtVo(M%=iJw^-~08t|APCc?+^2ubA7JQ^XGl8>vLTvEx^xH zce2T30D!Kym&bYlQ22-fh9>+QPIx&6|FmPhHpKxj#en`H;9M>n5|Oy|o-4s0^`<>= zfJLrYy8?he#Zy#alK`0M>Fu#1C;|EW;eL#yIj>9M{=LFl)ASnW=G9$@7W36VTVDjv zo$eK>m9y5zyXxg%&FXig2D+o~;hOF}zu4XAd62U*$RK=fEq*(@{@lLy4e7^!vkL$1 zx_gI%?Ma@Sv^H)@S7N9=-@Gz-@#fcE&$_;~ELHYiO2)vE!8Is&!~%es2>^~`&@(Fl z%*_$#uQmV-F97&_7$BFRDfMlD7**#@ z`97t;U-ilo((|yn zQhn#w4W@ixF{1JSSpDZn_jPqCRV_KrCcisu z#1N((&7+FHW(7QnsX})$fvjNgfNDmJ+V-<>kh{u!df^!WG~@b&!e4hi*wnRA9mt$= z+5>>(1j>lYZym3a7*x8mLtSNiKS$;- z3U!2XkCnIWECLo4X)^4dDQ~B%0t)+L)I9?Yx*fYQ5S*@vG-&3`oTTh{)@9HUi-GP4 z)!U$?ovEtSsHYYHcQO_p+oMF+^^e^SntTP|M@78wyXrop_j7{*0&<@!oz5o4%Z#XD z_2UP*DDdNwBHOl^10OFmA$7h0_;blMVf`FT_8tQRGl_uuU*cg_NUXt4W|v;cHk0dp6kA><1gdSzn3B^q)EKY%(j1^i%Os24Fl<2BvF; z+HAK3=4`^>o9ys4LhDxnzPViT!d3as7B7QLkCcJGYYDyTJOCfaCF?(omj{A47HD)K z{A+U2Y%#!(84G>l`zdXjJu0Y{klw$a6O|kYh9FnL*=p+DogF&)zqb?}WO>-pQ*4O*}u4|^{s)vA#)P>}4ec;{4-LfGj zXvCuki8j7J!F0eC13?S^Rz~_EC6)UCoD2A~kdXXvZ?Elh&1O*N4)~WW#lo{<@M#%R zD48|EalV^q+ig;yiwew8FgIK!zRN8BkDX}_q`yAcIpETMfB_1RD0nrt_@e)qktrqh zd0=O(t_&JAtXJB1`r-TUPK!N=g2~%e<4bfKHN_hF`2b&OOm+N6<#Gf}awKaFpx8{= zFcgz3i{wz{T}=TkwpF#U%MV}Vf^s3_Q7+lH+lD^_n_!eFW8~5J*a6c4C{|aejHy40 ze;smZ@6iU?Cl#Dypiokc`1=Fd8W9znixx_DfXg;OpeYvyf5D=XzelcE1Ho3gFo7OH zPG2B9BBByB>qlpkwt^89DOhgRk|$jx!te_r=X?-wUbcFO-p&%#gc#I(&Yo zh2e`jBYhG1Hd#wA-F0%i;e_R-C|JbYty*?z3u6x2+qo3)VSwPkPl|>wt>rmBpBC~z zz&N1fj#M{t(^aIM!T(^2WS;=|G-ImTM;FASFV})Ko-xxhYnyrO8ENq?-Hx^NEeGXd{(?QK^Ux)!afoi;C z3gYXk+wO_~S8n)7m;6 znFiAXrBp{1q5f0x#QFSaShB2Hpos}GMbzb7O@S9GFhTJF5!LB7{?F~8bG}&%tb0H0 zmO+jq+f^y+{?**^FCjve68N_Pl7|*%!3f*|^OSTeAGvvZVKZI-5jEMX}qbtG7 zNVE_{+w1*f$S;SPha63t&8QvCe_SeUJvi@&vOOIG=_C55_s_(aA-x?8nX=wGDyWZ; zmX?k@t{&XjHp)#j`>0wZ%o&?Em^f3`S0v5%Ipwc$djGaxl2Ab+iqBp(gR&D|%8`~U z{hi=C7wO`LlJR9xvSP@#4y)IL)tW7H9ov{*D?0SFq zWMOUn(VN3nyu&;PGlU-~qQ>^<@xxJKRk}Mko z&$wh|-^%s`_VDt-#njTFqEf@r=0l`v)Z?((=fJJ5&@DFAv)9O|wJfgd<(wz+vl9m% zYF)K5vfr}TLU44@7Ehdu@4m~(>@}vMJFfQ*l@dlX_OHtn8=9J)9&tVja{;SH=!2kV z%B+tibLON$%4v?)gYn+mX}c4c%vX-2UF{C^WzB6Mw+4l$6p_=m7!>%y8Z1Eg`$W9! zPp<%<10jyJahM?F52RqOT(WPk-khZ*^Wqo8tM>i7C$5<6?t4uyXSm0(G$ZZ}HaA>u zq56(KcEP}RT(WKjyRMeNtF6JDe8rW8j|3+Fs(vowIWL*7b5HzdzA0FOvGYMeuw1fX zFDtWczc^sJX05`dUBLk9+f-?M_VD=d;x_;LMU!y{ND5bdtZClZ-702lmXhSr;W6>@ zsR=qL{ySUf@_h;S&Hd5Dd*7VIH;j`(0b9;_-TWi_!AN~v&xiPmEZPo3&XKP>#15fG z%Z6+9^8I!E0e2ap*L2-l2a_dW;I<lMFCB5)Q3J!n2|JZ)#DoJK)+9dL8p3RLf4FA~-UpM>|!w;pVs6 z9AqDX8qc_pZ>5Q&JGUmF_;X|7oY1R;q)by_isI|&Q4JyaFuop$RsgOEEeuz?L(=%k z0vP4G5?Au==ulVF&F@QJz~%0_aQCB4KeB~#!W-B51*0u z_;y`gM5#i?I7TrvzRWqpw z%6Lq>P;qDMTGB4{j4U8H|Adg%^J;tO&4BHHosdY!q=U0c$qa80JC~5Y9r@_xh6ePy zyfU+a5q+!8$yY62gvhkz!Z-cCWmk|vXS1#b2`_P+9PhvKed(_6oG#B73*CnMA4wHu zj7%Fgb-RokyBzi{J+Pm-x{xtK5 z$vn6#PN0IXC8JKM=Z@KRHTm=A5B@0|eLz0TS=&#m*E zO)o~^A)DHrhFAHN8<^J`p?@tcv34XQVx_Ch;ntFGP#jz%{_yowRg(kR!2~PdGMkDw zol%_wmzMVweJ7Vf7XZOq&WgRa-ai9&Pi{o083OxI9)}gXzT#TB$7t^%dAO?93YctH zb=jc#LA@+Iu_aIFNpQ&yeURZucCg1z7CX-y{laKQbQ+XdYr(=N$}DT%w$75Q+>AL= zrQcPLcf}&%D8>&L2{#QYyKJ5*f@?fMh!)qB2-QLwnMCMxLU`{~KQ>2EmHuP6s?P$L zgsEz`s*LW(b@|Ob(*$xH$bBrJ%OxWm$jEu43vm!EQ512( z?N&sa84}<^lX#+2{>V18-CHhQixvo=J$JE?RC|_p+!$VlEcoHx>4Z9EsV^{ms?3TI zDHdv)ckb2NWrmVIAlVJ`WJCsF(4J8+$QOhliC>2D^B$&nD z!d1pZ2%Ad4WOcAOou<^<3jdov6$a)!U9Jztrm``a2u}F0DT|7jwLUyu z=EtRg;ADa9G=;sdH^KWPUU%=5Jyy4SPv7gCFC7U^4#@r)PNfk%x%j)^*K3%IsaMxG z9_Ow6Q=1Tj>})t!L-0Dqo6_~QUMmfbg6vF4(j|CR;^kLfe8IS+hb5Q|16tpt^dF6@ zJAZl^BH}$~;My?mmo8+SDkRW0EH;D3(#B81a=&yX7v zVIqO~np|=q8?b5Ljwt$HnjwK7CP%40rUEuZh3`d3GDQM)ZYff1v~ifgTOLpu>AXdn zwEWffe*%G=*LX7F;?f*TKM)HUZ1iL}+GWeS%QP)2&i+YAS`UHwGJ=|FY0*QC1F`BEc`p?e$({lv1ZR2M(1J zqfZs*lRnF-CryN;P4c&w?6M~i@ouaJc0Fp((9teNl=m~}%HJabmDqJTnP+Qht~YXz z_9HkC1V3sms#v0WeQLT~db_3{n%lIH%)4qS$m`1t^?PMyCnhr<+(?c~T-V%87<#ri$Be4qq!FVM-&jWGIamsQn*}Ry z%mUO8h&iQBGB%&dux}S-5J!$wbfFmz1Z*vZGkGx1#QnZR zv5=iV3ru;hR<*pm73GP-6C#YJ*GtyajIf356cF5gyY0@`m(nJWble1DHqXMcUD-H8 z$QHw?*8UbZw2_5GM1`rA<*eq%V=x7GF&U~yHj*s-1S5N+FVXha3LR|k>VpLm)k-QR zZ9eo-;&yU@j-B^$IQQw1Y~IE{Zm1veow?*`84FavEwBWh9_sHwZ+P&0VOOk9zG--p zk-ZuMTJ-fzy9vBxumscJBrQ2=jmoD*tCm^Hl!0bQv8?{+8;J>vdm@!+OEX2_<%Tvs zX-gS%$WXT1QV@KyT58|`bzag3Q~s&w4g56?4`WlaPSVo1ZYCFPN#7@JnOsj*3zsLZ z1-X?U;PKzKtxY&O7_o7@Qk3}HAx%%@go}Q$86jLha)d|d4c&^3OdzUB4L3jRhFmH6 zq^Jm(dIi*H>QSL8f2wn#EHHO!ccjW6cCFJrL4C1$aKk+^j&k5VJBi;mrhXOiPFJ%a zqMVThWS4Ml?k6vqx16#f^dk0P1=aOO=Tl+!?Ow89MU=Ok^Aw((^at1BwN%v|aNI>- zpz;q_o&V_su(RMyN}!uJKO?ml4(}|K?xXOPdH9MNt1h8xDj)Y)nrU-M8)IQ%G6-Jb zHYwA8(x*T)cfn>X1P3L*#%eMFb|Mc3?7-{X%8kg;|JYyPc(ApIjOumrU2jozrl5 z+B`5Z+@Ou?rov%?1ooYZD-o!`9X>OH>y%lw%BH0V7qA-rv0S)JM8J~;(DA^A4G>R} zuuQ~T$G{O}*E2(GqlG?snG51l*?{uFCgpR+i| zEa^HtA}rFzSx|P1&)uKooOd0DvQ$_UBlV>tzS~vjccOxAv=ej0iWIm>X;1iPD(7rv z0s|JJ_K>MWyvG(`wiP;-hL5s^sjI-6HMCz%P9ziV3qLcuZP3f%DvQ|&*MN3k)Sz?^ zK?SR6rEIA54cwwU_&gTZmC$40D6L=MBIi88_!cy|cnP7m7vry(cy=T7Ol`pAS+uQJ z#=`b!w17X6p?rmw-z+fMnx06KbAmm9;e->%A(I_i5KA+;)F?F$WBf_9rr5_yjanu+ z!-Nsw3azs?V6G)Pw(0~R^lq#KL94Oh<7@=#IRgqaVIpneTTwg}39w}~+ONdeLctGc z!B)$H&{~#2_48T={uElBzZxo9WDQKr(Xl&*P?-G-h&saPZk`aBgBDmofoHUUg*GmS zVLHwzXaNT(04-S40`l1agNFPl%RU5fDSS&jV(G?V7{TA}58 zGX>i5JZjMo>K;y0a*_JvrcdP~t-kOCA?M7=2LfwL^F@T-=h(^k8JC&NEJey+46tD~ z`nLnwXoko(d+;G3-&w))8Y4=gNZGFq7FnY2B53g98ELSE6h=vafN5DUa{{+ItFf05 zg!GXz4L@j1Lze5p3g8fW@Fcd{0__c>(JG7b;EzIVTLbnP#vEw#4F)b4ARP`Q4g~s^ z1<>RZ4E)MMhMY*gCp$^tG&Hk+0R_hMX|~IjAj)R~#anuRP`4Xu{a+sRTdri|`dUP;K7 zk1b`N7-e7M?JxTBUbe~}sOEdH4 z3|df&sm_pt{drybG(y8V6b-D*aDiR3Flj;gc$;Ki=a>e50+>HofWZ#|E>BoLtELIf z$Q@!oXSl`1@&aZfp!)roGG|!K;e{;G9PE}Vh{&cLyo28cb{@C)*C|;A;C5Z%=0)T4 z@VEQPl_Ppm^+$zp!7`)#C8nbr7hLoV?cnt>c>(Zx6d+4hX#e*!-T(PEN%FS;V?Vg# UyXE=t6D#mu<>&FoO2IGx1G&lE8vp with Serializable< ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); static const quantex = ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png'); + static const stealthEx = + ExchangeProviderDescription(title: 'StealthEx', raw: 10, image: 'assets/images/stealthex.png'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -50,6 +52,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< return thorChain; case 9: return quantex; + case 10: + return stealthEx; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart new file mode 100644 index 000000000..3e05091e6 --- /dev/null +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -0,0 +1,299 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:http/http.dart' as http; + +class StealthExExchangeProvider extends ExchangeProvider { + StealthExExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static const List _notSupported = []; + + static final apiKey = secrets.stealthExBearerToken; + static final _additionalFeePercent = double.tryParse(secrets.stealthExAdditionalFeePercent); + static const _baseUrl = 'https://api.stealthex.io'; + static const _rangePath = '/v4/rates/range'; + static const _amountPath = '/v4/rates/estimated-amount'; + static const _exchangesPath = '/v4/exchanges'; + + @override + String get title => 'StealthEX'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.stealthEx; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final curFrom = isFixedRateMode ? to : from; + final curTo = isFixedRateMode ? from : to; + + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + final body = { + 'route': { + 'from': {'symbol': _getName(curFrom), 'network': _getNetwork(curFrom)}, + 'to': {'symbol': _getName(curTo), 'network': _getNetwork(curTo)} + }, + 'estimation': isFixedRateMode ? 'reversed' : 'direct', + 'rate': isFixedRateMode ? 'fixed' : 'floating', + 'additional_fee_percent': _additionalFeePercent, + }; + + try { + final response = await http.post(Uri.parse(_baseUrl + _rangePath), + headers: headers, body: json.encode(body)); + if (response.statusCode != 200) { + throw Exception('StealthEx fetch limits failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final min = responseJSON['min_amount'] as double?; + final max = responseJSON['max_amount'] as double?; + return Limits(min: min, max: max); + } catch (e) { + log(e.toString()); + throw Exception('StealthEx failed to fetch limits'); + } + } + + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + final response = await getEstimatedExchangeAmount( + from: from, to: to, amount: amount, isFixedRateMode: isFixedRateMode); + final estimatedAmount = response['estimated_amount'] as double? ?? 0.0; + return estimatedAmount > 0.0 + ? isFixedRateMode + ? amount / estimatedAmount + : estimatedAmount / amount + : 0.0; + } + + @override + Future createTrade( + {required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll}) async { + String? rateId; + String? validUntil; + + try { + if (isFixedRateMode) { + final response = await getEstimatedExchangeAmount( + from: request.fromCurrency, + to: request.toCurrency, + amount: double.parse(request.toAmount), + isFixedRateMode: isFixedRateMode); + rateId = response['rate_id'] as String?; + validUntil = response['valid_until'] as String?; + if (rateId == null) throw TradeNotCreatedException(description); + } + + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + final body = { + 'route': { + 'from': { + 'symbol': _getName(request.fromCurrency), + 'network': _getNetwork(request.fromCurrency) + }, + 'to': {'symbol': _getName(request.toCurrency), 'network': _getNetwork(request.toCurrency)} + }, + 'estimation': isFixedRateMode ? 'reversed' : 'direct', + 'rate': isFixedRateMode ? 'fixed' : 'floating', + if (isFixedRateMode) 'rate_id': rateId, + 'amount': + isFixedRateMode ? double.parse(request.toAmount) : double.parse(request.fromAmount), + 'address': request.toAddress, + 'refund_address': request.refundAddress, + 'additional_fee_percent': _additionalFeePercent, + }; + + final response = await http.post(Uri.parse(_baseUrl + _exchangesPath), + headers: headers, body: json.encode(body)); + + if (response.statusCode != 201) { + throw Exception('StealthEx create trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final deposit = responseJSON['deposit'] as Map; + final withdrawal = responseJSON['withdrawal'] as Map; + + final id = responseJSON['id'] as String; + final from = deposit['symbol'] as String; + final to = withdrawal['symbol'] as String; + final payoutAddress = withdrawal['address'] as String; + final depositAddress = deposit['address'] as String; + final refundAddress = responseJSON['refund_address'] as String; + final depositAmount = toDouble(deposit['amount']); + final receiveAmount = toDouble(withdrawal['amount']); + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + + final createdAt = DateTime.parse(createdAtString); + final expiredAt = validUntil != null + ? DateTime.parse(validUntil) + : DateTime.now().add(Duration(minutes: 5)); + + + CryptoCurrency fromCurrency; + if (request.fromCurrency.tag != null && request.fromCurrency.title.toLowerCase() == from) { + fromCurrency = request.fromCurrency; + } else { + fromCurrency = CryptoCurrency.fromString(from); + } + + CryptoCurrency toCurrency; + if (request.toCurrency.tag != null && request.toCurrency.title.toLowerCase() == to) { + toCurrency = request.toCurrency; + } else { + toCurrency = CryptoCurrency.fromString(to); + } + + return Trade( + id: id, + from: fromCurrency, + to: toCurrency, + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount.toString(), + receiveAmount: receiveAmount.toString(), + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + expiredAt: expiredAt, + ); + } catch (e) { + log(e.toString()); + throw TradeNotCreatedException(description); + } + } + + @override + Future findTradeById({required String id}) async { + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + + final uri = Uri.parse('$_baseUrl$_exchangesPath/$id'); + final response = await http.get(uri, headers: headers); + + if (response.statusCode != 200) { + throw Exception('StealthEx fetch trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final deposit = responseJSON['deposit'] as Map; + final withdrawal = responseJSON['withdrawal'] as Map; + + final respId = responseJSON['id'] as String; + final from = deposit['symbol'] as String; + final to = withdrawal['symbol'] as String; + final payoutAddress = withdrawal['address'] as String; + final depositAddress = deposit['address'] as String; + final refundAddress = responseJSON['refund_address'] as String; + final depositAmount = toDouble(deposit['amount']); + final receiveAmount = toDouble(withdrawal['amount']); + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + final createdAt = DateTime.parse(createdAtString); + + return Trade( + id: respId, + from: CryptoCurrency.fromString(from), + to: CryptoCurrency.fromString(to), + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount.toString(), + receiveAmount: receiveAmount.toString(), + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + isRefund: status == 'refunded', + ); + } + + Future> getEstimatedExchangeAmount( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode}) async { + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + + final body = { + 'route': { + 'from': {'symbol': _getName(from), 'network': _getNetwork(from)}, + 'to': {'symbol': _getName(to), 'network': _getNetwork(to)} + }, + 'estimation': isFixedRateMode ? 'reversed' : 'direct', + 'rate': isFixedRateMode ? 'fixed' : 'floating', + 'amount': amount, + 'additional_fee_percent': _additionalFeePercent, + }; + + try { + final response = await http.post(Uri.parse(_baseUrl + _amountPath), + headers: headers, body: json.encode(body)); + if (response.statusCode != 200) return {}; + final responseJSON = json.decode(response.body) as Map; + final rate = responseJSON['rate'] as Map?; + return { + 'estimated_amount': responseJSON['estimated_amount'] as double?, + if (rate != null) 'valid_until': rate['valid_until'] as String?, + if (rate != null) 'rate_id': rate['id'] as String? + }; + } catch (e) { + log(e.toString()); + return {}; + } + } + + double toDouble(dynamic value) { + if (value is int) { + return value.toDouble(); + } else if (value is double) { + return value; + } else { + return 0.0; + } + } + + String _getName(CryptoCurrency currency) { + if (currency == CryptoCurrency.usdcEPoly) return 'usdce'; + return currency.title.toLowerCase(); + } + + String _getNetwork(CryptoCurrency currency) { + if (currency.tag == null) return 'mainnet'; + + if (currency == CryptoCurrency.maticpoly) return 'mainnet'; + + if (currency.tag == 'POLY') return 'matic'; + + return currency.tag!.toLowerCase(); + } +} diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index 0a196835e..e94906763 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -40,7 +40,6 @@ class TradeState extends EnumerableItem with Serializable { static const exchanging = TradeState(raw: 'exchanging', title: 'Exchanging'); static const sending = TradeState(raw: 'sending', title: 'Sending'); static const success = TradeState(raw: 'success', title: 'Success'); - static TradeState deserialize({required String raw}) { switch (raw) { @@ -119,6 +118,7 @@ class TradeState extends EnumerableItem with Serializable { case 'refunded': return refunded; case 'confirmation': + case 'verifying': return confirmation; case 'confirmed': return confirmed; diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index c05839578..4ceebedfd 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -16,7 +16,8 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap = true, displayTrocador = true, displayExolix = true, - displayThorChain = true; + displayThorChain = true, + displayStealthEx = true; @observable bool displayXMRTO; @@ -42,6 +43,9 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayThorChain; + @observable + bool displayStealthEx; + @computed bool get displayAllTrades => displayChangeNow && @@ -49,7 +53,8 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap && displayTrocador && displayExolix && - displayThorChain; + displayThorChain && + displayStealthEx; @action void toggleDisplayExchange(ExchangeProviderDescription provider) { @@ -78,6 +83,9 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.thorChain: displayThorChain = !displayThorChain; break; + case ExchangeProviderDescription.stealthEx: + displayStealthEx = !displayStealthEx; + break; case ExchangeProviderDescription.all: if (displayAllTrades) { displayChangeNow = false; @@ -88,6 +96,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = false; displayExolix = false; displayThorChain = false; + displayStealthEx = false; } else { displayChangeNow = true; displaySideShift = true; @@ -97,6 +106,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = true; displayExolix = true; displayThorChain = true; + displayStealthEx = true; } break; } @@ -112,13 +122,19 @@ abstract class TradeFilterStoreBase with Store { ? _trades .where((item) => (displayXMRTO && item.trade.provider == ExchangeProviderDescription.xmrto) || - (displaySideShift && item.trade.provider == ExchangeProviderDescription.sideShift) || - (displayChangeNow && item.trade.provider == ExchangeProviderDescription.changeNow) || - (displayMorphToken && item.trade.provider == ExchangeProviderDescription.morphToken) || - (displaySimpleSwap && item.trade.provider == ExchangeProviderDescription.simpleSwap) || + (displaySideShift && + item.trade.provider == ExchangeProviderDescription.sideShift) || + (displayChangeNow && + item.trade.provider == ExchangeProviderDescription.changeNow) || + (displayMorphToken && + item.trade.provider == ExchangeProviderDescription.morphToken) || + (displaySimpleSwap && + item.trade.provider == ExchangeProviderDescription.simpleSwap) || (displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador) || (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) || - (displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain)) + (displayThorChain && + item.trade.provider == ExchangeProviderDescription.thorChain) || + (displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx)) .toList() : _trades; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index d58d7535c..dc96c4461 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,12 +1,14 @@ import 'dart:convert'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/buy/buy_provider.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'; +import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/provider_types.dart'; -import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/service_status.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -45,11 +47,9 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:flutter/services.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:http/http.dart' as http; +import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; part 'dashboard_view_model.g.dart'; @@ -129,6 +129,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.thorChain.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)), + FilterItem( + value: () => tradeFilterStore.displayStealthEx, + caption: ExchangeProviderDescription.stealthEx.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)), ] }, subname = '', diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 5d99ff8a5..4cb7e4cad 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; @@ -52,6 +53,8 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.quantex: _provider = QuantexExchangeProvider(); break; + case ExchangeProviderDescription.stealthEx: + _provider = StealthExExchangeProvider(); case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 2bbe9954e..bd9474e39 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; +import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -160,15 +161,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final SharedPreferences sharedPreferences; List get _allProviders => [ - ChangeNowExchangeProvider(settingsStore: _settingsStore), - SideShiftExchangeProvider(), - SimpleSwapExchangeProvider(), - ThorChainExchangeProvider(tradesStore: trades), - if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), - QuantexExchangeProvider(), - TrocadorExchangeProvider( - useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), - ]; + ChangeNowExchangeProvider(settingsStore: _settingsStore), + SideShiftExchangeProvider(), + SimpleSwapExchangeProvider(), + ThorChainExchangeProvider(tradesStore: trades), + if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), + QuantexExchangeProvider(), + StealthExExchangeProvider(), + TrocadorExchangeProvider( + useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), + ]; @observable ExchangeProvider? provider; diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index eed1b6c75..e71d97ae0 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; @@ -60,6 +61,9 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.quantex: _provider = QuantexExchangeProvider(); break; + case ExchangeProviderDescription.stealthEx: + _provider = StealthExExchangeProvider(); + break; } _updateItems(); @@ -86,6 +90,8 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://track.ninerealms.com/${trade.id}'; case ExchangeProviderDescription.quantex: return 'https://myquantex.com/send/${trade.id}'; + case ExchangeProviderDescription.stealthEx: + return 'https://stealthex.io/exchange/?id=${trade.id}'; } return null; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index a1d93fcf9..0741ab4e7 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -43,6 +43,8 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('stealthExBearerToken', () => ''), + SecretKey('stealthExAdditionalFeePercent', () => ''), ]; static final evmChainsSecrets = [ From 125fdf8b624ce931e39905045d69b6312558a983 Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 9 Sep 2024 21:08:21 +0300 Subject: [PATCH 073/142] quick fix for the reconnection issue (#1671) * Update electrum.dart * Update electrum.dart * Duration seconds: 5 [skip ci] --- cw_bitcoin/lib/electrum.dart | 10 +++++++--- cw_bitcoin/lib/electrum_wallet.dart | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index bc630a4e1..8dfb8e01f 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -113,9 +113,13 @@ class ElectrumClient { }, onDone: () { unterminatedString = ''; - if (host == socket?.address.host) { - socket = null; - _setConnectionStatus(ConnectionStatus.disconnected); + try { + if (host == socket?.address.host) { + socket?.destroy(); + _setConnectionStatus(ConnectionStatus.disconnected); + } + } catch(e) { + print(e.toString()); } }, cancelOnError: true, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 73ae7c38f..b9bb0deb4 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2058,7 +2058,7 @@ abstract class ElectrumWalletBase _isTryingToConnect = true; - Timer(Duration(seconds: 10), () { + Timer(Duration(seconds: 5), () { if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) { this.electrumClient.connectToUri( node!.uri, From 215e7851981819166d53bf970777c70338bb2292 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 10 Sep 2024 21:39:07 +0300 Subject: [PATCH 074/142] Update electrum_wallet.dart (#1664) --- cw_bitcoin/lib/electrum_wallet.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index b9bb0deb4..191272b1f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1089,6 +1089,8 @@ abstract class ElectrumWalletBase }); } + unspentCoins.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); + await updateBalance(); }); } catch (e) { From 7d11d0461f5d32357572b8e8f1753f6c6c402aeb Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 11 Sep 2024 05:14:17 +0300 Subject: [PATCH 075/142] Integrate LetsExchange exchange provider (#1562) * letsExchange provider * add api key * secrets affiliateId * Update letsexchange_exchange_provider.dart * minor fix [skip ci] * fix network type issue * tracking link [skip ci] * fix data type * normalise bch address --------- Co-authored-by: Omar Hatem --- .github/workflows/pr_test_build_android.yml | 2 + .github/workflows/pr_test_build_linux.yml | 2 + assets/images/letsexchange_icon.svg | 5 + .../exchange_provider_description.dart | 6 +- .../letsexchange_exchange_provider.dart | 292 ++++++++++++++++++ .../stealth_ex_exchange_provider.dart | 2 +- lib/exchange/trade_state.dart | 2 + .../screens/dashboard/widgets/trade_row.dart | 4 +- .../exchange_trade/exchange_confirm_page.dart | 4 +- lib/store/dashboard/trade_filter_store.dart | 11 + lib/utils/image_utill.dart | 60 ++++ .../dashboard/dashboard_view_model.dart | 5 + .../exchange/exchange_view_model.dart | 2 + lib/view_model/trade_details_view_model.dart | 5 + tool/utils/secret_key.dart | 2 + 15 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 assets/images/letsexchange_icon.svg create mode 100644 lib/exchange/provider/letsexchange_exchange_provider.dart create mode 100644 lib/utils/image_utill.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index b60ae1f7e..1b26d87d0 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -168,6 +168,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index e25bea2ec..db88d7850 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -154,6 +154,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart diff --git a/assets/images/letsexchange_icon.svg b/assets/images/letsexchange_icon.svg new file mode 100644 index 000000000..104b43a6b --- /dev/null +++ b/assets/images/letsexchange_icon.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index a91288024..9f3723356 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -27,8 +27,10 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); static const quantex = ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png'); + static const letsExchange = + ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg'); static const stealthEx = - ExchangeProviderDescription(title: 'StealthEx', raw: 10, image: 'assets/images/stealthex.png'); + ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -53,6 +55,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< case 9: return quantex; case 10: + return letsExchange; + case 11: return stealthEx; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart new file mode 100644 index 000000000..a11e69796 --- /dev/null +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -0,0 +1,292 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:http/http.dart' as http; + +class LetsExchangeExchangeProvider extends ExchangeProvider { + LetsExchangeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static const List _notSupported = []; + + static const apiKey = secrets.letsExchangeBearerToken; + static const _baseUrl = 'api.letsexchange.io'; + static const _infoPath = '/api/v1/info'; + static const _infoRevertPath = '/api/v1/info-revert'; + static const _createTransactionPath = '/api/v1/transaction'; + static const _createTransactionRevertPath = '/api/v1/transaction-revert'; + static const _getTransactionPath = '/api/v1/transaction'; + + static const _affiliateId = secrets.letsExchangeAffiliateId; + + @override + String get title => 'LetsExchange'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.letsExchange; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final networkFrom = _getNetworkType(from); + final networkTo = _getNetworkType(to); + + try { + final params = { + 'from': from.title, + 'to': to.title, + if (networkFrom != null) 'network_from': networkFrom, + if (networkTo != null) 'network_to': networkTo, + 'amount': '1', + 'affiliate_id': _affiliateId + }; + + final responseJSON = await _getInfo(params, isFixedRateMode); + final min = double.tryParse(responseJSON['min_amount'] as String); + final max = double.tryParse(responseJSON['max_amount'] as String); + return Limits(min: min, max: max); + } catch (e) { + log(e.toString()); + throw Exception('Failed to fetch limits'); + } + } + + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + final networkFrom = _getNetworkType(from); + final networkTo = _getNetworkType(to); + try { + final params = { + 'from': from.title, + 'to': to.title, + if (networkFrom != null) 'network_from': networkFrom, + if (networkTo != null) 'network_to': networkTo, + 'amount': amount.toString(), + 'affiliate_id': _affiliateId + }; + + final responseJSON = await _getInfo(params, isFixedRateMode); + + final amountToGet = double.tryParse(responseJSON['amount'] as String) ?? 0.0; + + return isFixedRateMode ? amount / amountToGet : amountToGet / amount; + } catch (e) { + log(e.toString()); + return 0.0; + } + } + + @override + Future createTrade( + {required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll}) async { + final networkFrom = _getNetworkType(request.fromCurrency); + final networkTo = _getNetworkType(request.toCurrency); + try { + final params = { + 'from': request.fromCurrency.title, + 'to': request.toCurrency.title, + if (networkFrom != null) 'network_from': networkFrom, + if (networkTo != null) 'network_to': networkTo, + 'amount': isFixedRateMode ? request.toAmount.toString() : request.fromAmount.toString(), + 'affiliate_id': _affiliateId + }; + + final responseInfoJSON = await _getInfo(params, isFixedRateMode); + final rateId = responseInfoJSON['rate_id'] as String; + + final withdrawalAddress = _normalizeBchAddress(request.toAddress); + final returnAddress = _normalizeBchAddress(request.refundAddress); + + final tradeParams = { + 'coin_from': request.fromCurrency.title, + 'coin_to': request.toCurrency.title, + if (!isFixedRateMode) 'deposit_amount': request.fromAmount.toString(), + 'withdrawal': withdrawalAddress, + if (isFixedRateMode) 'withdrawal_amount': request.toAmount.toString(), + 'withdrawal_extra_id': '', + 'return': returnAddress, + 'rate_id': rateId, + if (networkFrom != null) 'network_from': networkFrom, + if (networkTo != null) 'network_to': networkTo, + 'affiliate_id': _affiliateId + }; + + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': apiKey + }; + + final uri = Uri.https(_baseUrl, + isFixedRateMode ? _createTransactionRevertPath : _createTransactionPath, tradeParams); + final response = await http.post(uri, headers: headers); + + if (response.statusCode != 200) { + throw Exception('LetsExchange create trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final id = responseJSON['transaction_id'] as String; + final from = responseJSON['coin_from'] as String; + final to = responseJSON['coin_to'] as String; + final payoutAddress = responseJSON['withdrawal'] as String; + final depositAddress = responseJSON['deposit'] as String; + final refundAddress = responseJSON['return'] as String; + final depositAmount = responseJSON['deposit_amount'] as String; + final receiveAmount = responseJSON['withdrawal_amount'] as String; + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + final expiredAtTimestamp = responseJSON['expired_at'] as int; + + final createdAt = DateTime.parse(createdAtString); + final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000); + + CryptoCurrency fromCurrency; + if (request.fromCurrency.tag != null && request.fromCurrency.title == from) { + fromCurrency = request.fromCurrency; + } else { + fromCurrency = CryptoCurrency.fromString(from); + } + + CryptoCurrency toCurrency; + if (request.toCurrency.tag != null && request.toCurrency.title == to) { + toCurrency = request.toCurrency; + } else { + toCurrency = CryptoCurrency.fromString(to); + } + + return Trade( + id: id, + from: fromCurrency, + to: toCurrency, + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount, + receiveAmount: receiveAmount, + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + expiredAt: expiredAt, + ); + } catch (e) { + log(e.toString()); + throw TradeNotCreatedException(description); + } + } + + @override + Future findTradeById({required String id}) async { + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': apiKey + }; + + final url = Uri.https(_baseUrl, '$_getTransactionPath/$id'); + final response = await http.get(url, headers: headers); + + if (response.statusCode != 200) { + throw Exception('LetsExchange fetch trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final from = responseJSON['coin_from'] as String; + final to = responseJSON['coin_to'] as String; + final payoutAddress = responseJSON['withdrawal'] as String; + final depositAddress = responseJSON['deposit'] as String; + final refundAddress = responseJSON['return'] as String; + final depositAmount = responseJSON['deposit_amount'] as String; + final receiveAmount = responseJSON['withdrawal_amount'] as String; + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + final expiredAtTimestamp = responseJSON['expired_at'] as int; + + final createdAt = DateTime.parse(createdAtString); + final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000); + + return Trade( + id: id, + from: CryptoCurrency.fromString(from), + to: CryptoCurrency.fromString(to), + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount, + receiveAmount: receiveAmount, + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + expiredAt: expiredAt, + isRefund: status == 'refund', + ); + } + + Future> _getInfo(Map params, bool isFixedRateMode) async { + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': apiKey + }; + + try { + final uri = Uri.https(_baseUrl, isFixedRateMode ? _infoRevertPath : _infoPath, params); + final response = await http.post(uri, headers: headers); + if (response.statusCode != 200) { + throw Exception('LetsExchange fetch info failed: ${response.body}'); + } + return json.decode(response.body) as Map; + } catch (e) { + throw Exception('LetsExchange failed to fetch info ${e.toString()}'); + } + } + + String? _getNetworkType(CryptoCurrency currency) { + if (currency.tag != null && currency.tag!.isNotEmpty) { + switch (currency.tag!) { + case 'TRX': + return 'TRC20'; + case 'ETH': + return 'ERC20'; + case 'BSC': + return 'BEP20'; + case 'POLY': + return 'MATIC'; + default: + return currency.tag!; + } + } + return currency.title; + } + + String _normalizeBchAddress(String address) => + address.startsWith('bitcoincash:') ? address.substring(12) : address; +} diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index 3e05091e6..601735595 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -69,7 +69,7 @@ class StealthExExchangeProvider extends ExchangeProvider { throw Exception('StealthEx fetch limits failed: ${response.body}'); } final responseJSON = json.decode(response.body) as Map; - final min = responseJSON['min_amount'] as double?; + final min = toDouble(responseJSON['min_amount']); final max = responseJSON['max_amount'] as double?; return Limits(min: min, max: max); } catch (e) { diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index e94906763..6d2472a11 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -106,6 +106,7 @@ class TradeState extends EnumerableItem with Serializable { case 'waitingAuthorization': return waitingAuthorization; case 'failed': + case 'error': return failed; case 'completed': return completed; @@ -125,6 +126,7 @@ class TradeState extends EnumerableItem with Serializable { case 'exchanging': return exchanging; case 'sending': + case 'sending_confirmation': return sending; case 'success': case 'done': diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index caccb8047..7c809aa9d 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/utils/image_utill.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; @@ -36,7 +37,8 @@ class TradeRow extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(50), - child: Image.asset(provider.image, width: 36, height: 36)), + child: ImageUtil.getImageFromPath( + imagePath: provider.image, height: 36, width: 36)), SizedBox(width: 12), Expanded( child: Column( diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index 8070febdf..bf307dce6 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; @@ -101,7 +102,8 @@ class ExchangeConfirmPage extends BasePage { mainAxisAlignment: MainAxisAlignment.center, children: [ (trade.provider.image?.isNotEmpty ?? false) - ? Image.asset(trade.provider.image, height: 50) + ? ImageUtil.getImageFromPath( + imagePath: trade.provider.image, width: 50) : const SizedBox(), if (!trade.provider.horizontalLogo) Padding( diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index 4ceebedfd..c1e462cd6 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -17,6 +17,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = true, displayExolix = true, displayThorChain = true, + displayLetsExchange = true, displayStealthEx = true; @observable @@ -43,6 +44,9 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayThorChain; + @observable + bool displayLetsExchange; + @observable bool displayStealthEx; @@ -54,6 +58,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador && displayExolix && displayThorChain && + displayLetsExchange && displayStealthEx; @action @@ -83,6 +88,8 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.thorChain: displayThorChain = !displayThorChain; break; + case ExchangeProviderDescription.letsExchange: + displayLetsExchange = !displayLetsExchange; case ExchangeProviderDescription.stealthEx: displayStealthEx = !displayStealthEx; break; @@ -96,6 +103,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = false; displayExolix = false; displayThorChain = false; + displayLetsExchange = false; displayStealthEx = false; } else { displayChangeNow = true; @@ -106,6 +114,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = true; displayExolix = true; displayThorChain = true; + displayLetsExchange = true; displayStealthEx = true; } break; @@ -134,6 +143,8 @@ abstract class TradeFilterStoreBase with Store { (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) || (displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain) || + (displayLetsExchange && + item.trade.provider == ExchangeProviderDescription.letsExchange) || (displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx)) .toList() : _trades; diff --git a/lib/utils/image_utill.dart b/lib/utils/image_utill.dart new file mode 100644 index 000000000..a138df23a --- /dev/null +++ b/lib/utils/image_utill.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ImageUtil { + static Widget getImageFromPath({required String imagePath, double? height, double? width}) { + final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https'); + final bool isSvg = imagePath.endsWith('.svg'); + final double _height = height ?? 35; + final double _width = width ?? 35; + + if (isNetworkImage) { + return isSvg + ? SvgPicture.network( + imagePath, + height: _height, + width: _width, + placeholderBuilder: (BuildContext context) => Container( + height: _height, + width: _width, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ) + : Image.network( + imagePath, + height: _height, + width: _width, + loadingBuilder: + (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Container( + height: _height, + width: _width, + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { + return Container( + height: _height, + width: _width, + ); + }, + ); + } else { + return isSvg + ? SvgPicture.asset(imagePath, height: _height, width: _width) + : Image.asset(imagePath, height: _height, width: _width); + } + } +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index dc96c4461..d69d662e1 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -129,6 +129,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.thorChain.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)), + FilterItem( + value: () => tradeFilterStore.displayLetsExchange, + caption: ExchangeProviderDescription.letsExchange.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)), FilterItem( value: () => tradeFilterStore.displayStealthEx, caption: ExchangeProviderDescription.stealthEx.title, diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index bd9474e39..ca56750f0 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; +import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; @@ -167,6 +168,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ThorChainExchangeProvider(tradesStore: trades), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), QuantexExchangeProvider(), + LetsExchangeExchangeProvider(), StealthExExchangeProvider(), TrocadorExchangeProvider( useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index e71d97ae0..19315f40d 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; @@ -60,6 +61,8 @@ abstract class TradeDetailsViewModelBase with Store { break; case ExchangeProviderDescription.quantex: _provider = QuantexExchangeProvider(); + case ExchangeProviderDescription.letsExchange: + _provider = LetsExchangeExchangeProvider(); break; case ExchangeProviderDescription.stealthEx: _provider = StealthExExchangeProvider(); @@ -90,6 +93,8 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://track.ninerealms.com/${trade.id}'; case ExchangeProviderDescription.quantex: return 'https://myquantex.com/send/${trade.id}'; + case ExchangeProviderDescription.letsExchange: + return 'https://letsexchange.io/?transactionId=${trade.id}'; case ExchangeProviderDescription.stealthEx: return 'https://stealthex.io/exchange/?id=${trade.id}'; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 0741ab4e7..96787a403 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -43,6 +43,8 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('letsExchangeBearerToken', () => ''), + SecretKey('letsExchangeAffiliateId', () => ''), SecretKey('stealthExBearerToken', () => ''), SecretKey('stealthExAdditionalFeePercent', () => ''), ]; From e2bfb32f2c0a3b8edcce607309bdfa8582ac470c Mon Sep 17 00:00:00 2001 From: Leo Pham Date: Wed, 11 Sep 2024 11:50:41 +0700 Subject: [PATCH 076/142] Add Vietnamese Language (#1674) * Create strings_vi.arb Add vietnamese language * Update language_service.dart Add vietnamese language * Update language_service.dart * Update translation_constants.dart add vietnamese --------- Co-authored-by: Omar Hatem --- lib/entities/language_service.dart | 2 + res/values/strings_vi.arb | 900 ++++++++++++++++++ .../translation/translation_constants.dart | 2 +- 3 files changed, 903 insertions(+), 1 deletion(-) create mode 100644 res/values/strings_vi.arb diff --git a/lib/entities/language_service.dart b/lib/entities/language_service.dart index 4a6b358e1..78437fdce 100644 --- a/lib/entities/language_service.dart +++ b/lib/entities/language_service.dart @@ -31,6 +31,7 @@ class LanguageService { 'ha': 'Hausa Najeriya (Nigeria)', 'tl': 'Filipino (Tagalog)', 'hy': 'Հայերեն (Armenian)', + 'vi': 'Tiếng Việt (Vietnamese)', }; static const Map localeCountryCode = { @@ -61,6 +62,7 @@ class LanguageService { 'ha': 'hau', 'tl': 'phl', 'hy': 'arm', + 'vi': 'vnm', }; static final list = {}; diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb new file mode 100644 index 000000000..d2c98fe2b --- /dev/null +++ b/res/values/strings_vi.arb @@ -0,0 +1,900 @@ +{ + "about_cake_pay": "Cake Pay cho phép bạn dễ dàng mua thẻ quà tặng bằng tài sản ảo, có thể sử dụng ngay lập tức tại hơn 150.000 cửa hàng tại Hoa Kỳ.", + "account": "Tài khoản", + "accounts": "Tài khoản", + "accounts_subaddresses": "Tài khoản và địa chỉ phụ", + "activate": "Kích hoạt", + "active": "Đang hoạt động", + "active_cards": "Thẻ đang hoạt động", + "activeConnectionsPrompt": "Các kết nối đang hoạt động sẽ xuất hiện ở đây", + "add": "Thêm", + "add_contact": "Thêm liên hệ", + "add_contact_to_address_book": "Bạn có muốn thêm liên hệ này vào sổ địa chỉ không?", + "add_custom_node": "Thêm nút tùy chỉnh mới", + "add_custom_redemption": "Thêm đổi thưởng tùy chỉnh", + "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_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", + "add_token_disclaimer_check": "Tôi đã xác nhận địa chỉ hợp đồng token và thông tin bằng nguồn đáng tin cậy. Thêm thông tin sai hoặc độc hại có thể dẫn đến mất tiền.", + "add_token_warning": "Không chỉnh sửa hoặc thêm token theo yêu cầu của kẻ lừa đảo.\nLuôn xác nhận địa chỉ token từ các nguồn đáng tin cậy!", + "add_value": "Thêm giá trị", + "address": "Địa chỉ", + "address_book": "Sổ địa chỉ", + "address_book_menu": "Sổ địa chỉ", + "address_detected": "Địa chỉ được phát hiện", + "address_from_domain": "Địa chỉ này đến từ ${domain} trên Unstoppable Domains", + "address_from_yat": "Địa chỉ này đến từ ${emoji} trên Yat", + "address_label": "Nhãn địa chỉ", + "address_remove_contact": "Xóa liên hệ", + "address_remove_content": "Bạn có chắc chắn muốn xóa liên hệ đã chọn không?", + "addresses": "Địa chỉ", + "advanced_settings": "Cài đặt nâng cao", + "aggressive": "Tích cực", + "agree": "Đồng ý", + "agree_and_continue": "Đồng ý & Tiếp tục", + "agree_to": "Bằng cách tạo tài khoản, bạn đồng ý với ", + "all": "TẤT CẢ", + "all_trades": "Tất cả giao dịch", + "all_transactions": "Tất cả giao dịch", + "alphabetical": "Theo thứ tự chữ cái", + "already_have_account": "Bạn đã có tài khoản?", + "always": "Luôn luôn", + "amount": "Số tiền: ", + "amount_is_below_minimum_limit": "Số dư của bạn sau khi trừ phí sẽ thấp hơn số tiền tối thiểu cần thiết cho giao dịch (${min})", + "amount_is_estimate": "Số tiền nhận được là ước tính", + "amount_is_guaranteed": "Số tiền nhận được được đảm bảo", + "and": "và", + "anonpay_description": "Tạo ${type}. Người nhận có thể ${method} với bất kỳ loại tiền điện tử nào được hỗ trợ, và bạn sẽ nhận được tiền trong ví này.", + "apk_update": "Cập nhật APK", + "approve": "Phê duyệt", + "arrive_in_this_address": "${currency} ${tag} sẽ đến địa chỉ này", + "ascending": "Tăng dần", + "ask_each_time": "Hỏi mỗi lần", + "auth_store_ban_timeout": "thời gian cấm", + "auth_store_banned_for": "Bị cấm trong ", + "auth_store_banned_minutes": " phút", + "auth_store_incorrect_password": "Sai PIN", + "authenticated": "Đã xác thực", + "authentication": "Xác thực", + "auto_generate_addresses": "Tự động tạo địa chỉ", + "auto_generate_subaddresses": "Tự động tạo địa chỉ phụ", + "automatic": "Tự động", + "available_balance": "Số dư khả dụng", + "available_balance_description": "Số dư khả dụng hoặc số dư đã xác nhận là số tiền có thể chi tiêu ngay lập tức. Nếu tiền xuất hiện ở số dư thấp hơn nhưng không phải ở số dư cao hơn, bạn phải đợi một vài phút để các khoản tiền đến có thêm xác nhận từ mạng lưới. Sau khi có thêm xác nhận, chúng sẽ có thể chi tiêu.", + "avg_savings": "Tiết kiệm trung bình", + "awaitDAppProcessing": "Vui lòng đợi ứng dụng phi tập trung hoàn thành xử lý.", + "awaiting_payment_confirmation": "Đang chờ xác nhận thanh toán", + "background_sync_mode": "Chế độ đồng bộ nền", + "backup": "Sao lưu", + "backup_file": "Tập tin sao lưu", + "backup_password": "Mật khẩu sao lưu", + "balance": "Số dư", + "balance_page": "Trang số dư", + "bill_amount": "Số tiền hóa đơn", + "billing_address_info": "Nếu được yêu cầu địa chỉ thanh toán, hãy cung cấp địa chỉ giao hàng của bạn", + "biometric_auth_reason": "Quét vân tay để xác thực", + "bitcoin_dark_theme": "Chủ đề Bitcoin tối", + "bitcoin_light_theme": "Chủ đề Bitcoin sáng", + "bitcoin_payments_require_1_confirmation": "Các khoản thanh toán Bitcoin yêu cầu 1 xác nhận, có thể mất 20 phút hoặc lâu hơn. Cảm ơn bạn đã kiên nhẫn! Bạn sẽ nhận được email khi thanh toán được xác nhận.", + "block_remaining": "1 khối còn lại", + "Blocks_remaining": "${status} khối còn lại", + "bluetooth": "Bluetooth", + "bright_theme": "Sáng", + "bump_fee": "Tăng phí", + "buy": "Mua", + "buy_alert_content": "Hiện tại chúng tôi chỉ hỗ trợ mua Bitcoin, Ethereum, Litecoin, và Monero. Vui lòng tạo hoặc chuyển sang ví Bitcoin, Ethereum, Litecoin, hoặc Monero của bạn.", + "buy_bitcoin": "Mua Bitcoin", + "buy_now": "Mua ngay", + "buy_provider_unavailable": "Nhà cung cấp hiện không khả dụng.", + "buy_with": "Mua bằng", + "by_cake_pay": "bởi Cake Pay", + "cake_2fa_preset": "Thiết lập sẵn Cake 2FA", + "cake_dark_theme": "Chủ đề Cake tối", + "cake_pay_account_note": "Đăng ký chỉ với một địa chỉ email để xem và mua thẻ. Một số thẻ còn được giảm giá!", + "cake_pay_learn_more": "Mua và đổi thẻ quà tặng ngay trong ứng dụng!\nVuốt từ trái sang phải để tìm hiểu thêm.", + "cake_pay_save_order": "Thẻ sẽ được gửi đến email của bạn trong vòng 1 ngày làm việc \n Lưu mã Đơn hàng của bạn:", + "cake_pay_subtitle": "Mua thẻ trả trước toàn cầu và thẻ quà tặng", + "cake_pay_web_cards_subtitle": "Mua thẻ trả trước toàn cầu và thẻ quà tặng", + "cake_pay_web_cards_title": "Thẻ Cake Pay Web", + "cake_wallet": "Ví Cake", + "cakepay_prepaid_card": "Thẻ Ghi Nợ Trả Trước CakePay", + "camera_consent": "Máy ảnh của bạn sẽ được sử dụng để chụp hình nhằm mục đích xác minh danh tính bởi ${provider}. Vui lòng kiểm tra Chính sách quyền riêng tư của họ để biết thêm chi tiết.", + "camera_permission_is_required": "Cần có quyền truy cập máy ảnh. \nVui lòng bật nó từ cài đặt ứng dụng.", + "cancel": "Hủy", + "card_address": "Địa chỉ:", + "cardholder_agreement": "Thỏa thuận Chủ Thẻ", + "cards": "Thẻ", + "chains": "Chuỗi", + "change": "Thay đổi", + "change_backup_password_alert": "Các tệp sao lưu trước đây sẽ không còn khả dụng để nhập với mật khẩu sao lưu mới. Mật khẩu sao lưu mới chỉ được sử dụng cho các tệp sao lưu mới. Bạn có chắc chắn muốn thay đổi mật khẩu sao lưu không?", + "change_currency": "Thay đổi Tiền tệ", + "change_current_node": "Bạn có chắc chắn muốn thay đổi nút hiện tại sang ${node} không?", + "change_current_node_title": "Thay đổi nút hiện tại", + "change_exchange_provider": "Thay đổi Nhà cung cấp Trao đổi", + "change_language": "Thay đổi ngôn ngữ", + "change_language_to": "Thay đổi ngôn ngữ sang ${language}?", + "change_password": "Thay đổi mật khẩu", + "change_rep": "Thay đổi Đại diện", + "change_rep_message": "Bạn có chắc chắn muốn thay đổi đại diện không?", + "change_rep_successful": "Thay đổi đại diện thành công", + "change_wallet_alert_content": "Bạn có muốn thay đổi ví hiện tại thành ${wallet_name} không?", + "change_wallet_alert_title": "Thay đổi ví hiện tại", + "choose_account": "Chọn tài khoản", + "choose_address": "\n\nVui lòng chọn địa chỉ:", + "choose_card_value": "Chọn giá trị thẻ", + "choose_derivation": "Chọn Đường dẫn Ví", + "choose_from_available_options": "Chọn từ các tùy chọn có sẵn:", + "choose_one": "Chọn một", + "choose_relay": "Vui lòng chọn một relay để sử dụng", + "choose_wallet_currency": "Vui lòng chọn tiền tệ của ví:", + "clear": "Xóa", + "clearnet_link": "Liên kết Clearnet", + "close": "Đóng", + "coin_control": "Kiểm soát đồng xu (tùy chọn)", + "cold_or_recover_wallet": "Thêm ví lạnh hoặc khôi phục ví giấy", + "color_theme": "Chủ đề màu sắc", + "commit_transaction_amount_fee": "Cam kết giao dịch\nSố tiền: ${amount}\nPhí: ${fee}", + "confirm": "Xác nhận", + "confirm_delete_template": "Thao tác này sẽ xóa mẫu này. Bạn có muốn tiếp tục không?", + "confirm_delete_wallet": "Thao tác này sẽ xóa ví này. Bạn có muốn tiếp tục không?", + "confirm_fee_deduction": "Xác nhận Khấu trừ Phí", + "confirm_fee_dedction_content": "Bạn có đồng ý trừ phí từ đầu ra không?", + "confirm_sending": "Xác nhận gửi", + "confirm_silent_payments_switch_node": "Nút hiện tại của bạn không hỗ trợ thanh toán im lặng\\nCake Wallet sẽ chuyển sang một nút tương thích chỉ để quét", + "confirmations": "Xác nhận", + "confirmed": "Số dư đã xác nhận", + "confirmed_tx": "Đã xác nhận", + "congratulations": "Chúc mừng!", + "connect_an_existing_yat": "Kết nối Yat hiện có", + "connect_yats": "Kết nối Yats", + "connect_your_hardware_wallet": "Kết nối ví phần cứng của bạn bằng Bluetooth hoặc USB", + "connect_your_hardware_wallet_ios": "Kết nối ví phần cứng của bạn bằng Bluetooth", + "connection_sync": "Kết nối và đồng bộ hóa", + "connectWalletPrompt": "Kết nối ví của bạn với WalletConnect để thực hiện giao dịch", + "contact": "Liên hệ", + "contact_list_contacts": "Danh bạ", + "contact_list_wallets": "Ví của tôi", + "contact_name": "Tên liên hệ", + "contact_support": "Liên hệ Hỗ trợ", + "continue_text": "Tiếp tục", + "contractName": "Tên Hợp đồng", + "contractSymbol": "Ký hiệu Hợp đồng", + "copied_key_to_clipboard": "Đã sao chép ${key} vào khay nhớ tạm", + "copied_to_clipboard": "Đã sao chép vào khay nhớ tạm", + "copy": "Sao chép", + "copy_address": "Sao chép Địa chỉ", + "copy_id": "Sao chép ID", + "copyWalletConnectLink": "Sao chép liên kết WalletConnect từ dApp và dán vào đây", + "countries": "Quốc gia", + "create_account": "Tạo tài khoản", + "create_backup": "Tạo sao lưu", + "create_donation_link": "Tạo liên kết quyên góp", + "create_invoice": "Tạo hóa đơn", + "create_new": "Tạo Ví Mới", + "create_new_account": "Tạo tài khoản mới", + "creating_new_wallet": "Đang tạo ví mới", + "creating_new_wallet_error": "Lỗi: ${description}", + "creation_date": "Ngày Tạo", + "custom": "Tùy chỉnh", + "custom_drag": "Tùy chỉnh (Giữ và Kéo)", + "custom_redeem_amount": "Số tiền Chuộc Tùy chỉnh", + "custom_value": "Giá trị Tùy chỉnh", + "dark_theme": "Tối", + "debit_card": "Thẻ Ghi Nợ", + "debit_card_terms": "Việc lưu trữ và sử dụng số thẻ thanh toán của bạn (và thông tin xác thực tương ứng với số thẻ thanh toán của bạn) trong ví điện tử này phải tuân theo Điều khoản và Điều kiện của thỏa thuận chủ thẻ hiện hành với tổ chức phát hành thẻ thanh toán, theo thời gian.", + "decimal_places_error": "Quá nhiều chữ số thập phân", + "decimals_cannot_be_zero": "Chữ số thập phân không thể là số không.", + "default_buy_provider": "Nhà cung cấp Mua mặc định", + "default_sell_provider": "Nhà cung cấp Bán mặc định", + "delete": "Xóa", + "delete_account": "Xóa Tài khoản", + "delete_wallet": "Xóa ví", + "delete_wallet_confirm_message": "Bạn có chắc chắn muốn xóa ví ${wallet_name} không?", + "deleteConnectionConfirmationPrompt": "Bạn có chắc chắn muốn xóa kết nối với", + "denominations": "Mệnh giá", + "derivationpath": "Đường dẫn Derivation", + "descending": "Giảm dần", + "description": "Mô tả", + "destination_tag": "Thẻ đích:", + "dfx_option_description": "Mua tiền điện tử bằng EUR & CHF. Dành cho khách hàng bán lẻ và doanh nghiệp tại Châu Âu", + "didnt_get_code": "Không nhận được mã?", + "digit_pin": "Mã PIN - số", + "digital_and_physical_card": "thẻ ghi nợ trả trước kỹ thuật số và vật lý", + "disable": "Vô hiệu hóa", + "disable_bulletin": "Vô hiệu hóa bản tin tình trạng dịch vụ", + "disable_buy": "Vô hiệu hóa chức năng mua", + "disable_cake_2fa": "Vô hiệu hóa 2FA Cake", + "disable_exchange": "Vô hiệu hóa chức năng trao đổi", + "disable_fee_api_warning": "Khi tắt chức năng này, tỉ lệ phí có thể không chính xác trong một số trường hợp, dẫn đến bạn trả quá hoặc không đủ phí cho giao dịch của mình.", + "disable_fiat": "Vô hiệu hóa tiền tệ fiat", + "disable_sell": "Vô hiệu hóa chức năng bán", + "disableBatteryOptimization": "Vô hiệu hóa Tối ưu hóa Pin", + "disableBatteryOptimizationDescription": "Bạn có muốn vô hiệu hóa tối ưu hóa pin để đồng bộ hóa nền hoạt động mượt mà hơn không?", + "disabled": "Đã vô hiệu hóa", + "discount": "Tiết kiệm ${value}%", + "display_settings": "Cài đặt hiển thị", + "displayable": "Có thể hiển thị", + "do_not_have_enough_gas_asset": "Bạn không có đủ ${currency} để thực hiện giao dịch với điều kiện mạng blockchain hiện tại. Bạn cần thêm ${currency} để trả phí mạng blockchain, ngay cả khi bạn đang gửi tài sản khác.", + "do_not_send": "Đừng gửi", + "do_not_share_warning_text": "Không chia sẻ điều này với bất kỳ ai, bao gồm cả bộ phận hỗ trợ.\n\nTài sản của bạn có thể và sẽ bị đánh cắp!", + "do_not_show_me": "Không hiển thị lại cho tôi", + "domain_looks_up": "Tra cứu tên miền", + "donation_link_details": "Chi tiết liên kết quyên góp", + "e_sign_consent": "Đồng ý Ký Điện tử", + "edit": "Chỉnh sửa", + "edit_backup_password": "Chỉnh sửa mật khẩu sao lưu", + "edit_node": "Chỉnh sửa nút", + "edit_token": "Chỉnh sửa token", + "electrum_address_disclaimer": "Chúng tôi tạo địa chỉ mới mỗi khi bạn sử dụng, nhưng các địa chỉ cũ vẫn tiếp tục hoạt động", + "email_address": "Địa chỉ Email", + "enable_replace_by_fee": "Bật Thay thế Bằng Phí", + "enable_silent_payments_scanning": "Bật quét thanh toán im lặng", + "enabled": "Đã bật", + "enter_amount": "Nhập số tiền", + "enter_backup_password": "Nhập mật khẩu sao lưu tại đây", + "enter_code": "Nhập mã", + "enter_seed_phrase": "Nhập cụm từ hạt giống của bạn", + "enter_totp_code": "Vui lòng nhập mã TOTP.", + "enter_wallet_password": "Nhập mật khẩu ví", + "enter_your_note": "Nhập ghi chú của bạn...", + "enter_your_pin": "Nhập mã PIN của bạn", + "enter_your_pin_again": "Nhập lại mã PIN của bạn", + "enterTokenID": "Nhập ID token", + "enterWalletConnectURI": "Nhập URI WalletConnect", + "error": "Lỗi", + "error_dialog_content": "Oops, chúng tôi gặp một số lỗi.\n\nVui lòng gửi báo cáo sự cố cho nhóm hỗ trợ của chúng tôi để cải thiện ứng dụng.", + "error_text_account_name": "Tên tài khoản chỉ được chứa chữ cái, số\nvà phải từ 1 đến 15 ký tự", + "error_text_address": "Địa chỉ ví phải tương ứng với loại tiền điện tử", + "error_text_amount": "Số tiền chỉ được chứa số", + "error_text_contact_name": "Tên liên hệ không được chứa ký tự ` , ' \"\nvà phải từ 1 đến 32 ký tự", + "error_text_crypto_currency": "Số chữ số thập phân phải ít hơn hoặc bằng 12", + "error_text_fiat": "Giá trị số tiền không được vượt quá số dư khả dụng.\nSố chữ số thập phân phải ít hơn hoặc bằng 2", + "error_text_input_above_maximum_limit": "Số tiền vượt quá giới hạn tối đa", + "error_text_input_below_minimum_limit": "Số tiền dưới mức tối thiểu", + "error_text_keys": "Khóa ví chỉ được chứa 64 ký tự dạng hex", + "error_text_limits_loading_failed": "Giao dịch cho ${provider} không được tạo. Tải giới hạn không thành công", + "error_text_maximum_limit": "Giao dịch cho ${provider} không được tạo. Số tiền lớn hơn mức tối đa: ${max} ${currency}", + "error_text_minimal_limit": "Giao dịch cho ${provider} không được tạo. Số tiền nhỏ hơn mức tối thiểu: ${min} ${currency}", + "error_text_node_address": "Vui lòng nhập địa chỉ iPv4", + "error_text_node_port": "Cổng nút chỉ được chứa các số từ 0 đến 65535", + "error_text_node_proxy_address": "Vui lòng nhập <địa chỉ IPv4>:, ví dụ 127.0.0.1:9050", + "error_text_payment_id": "ID thanh toán chỉ được chứa từ 16 đến 64 ký tự dạng hex", + "error_text_subaddress_name": "Tên địa chỉ phụ không được chứa ký tự ` , ' \"\nvà phải từ 1 đến 20 ký tự", + "error_text_template": "Tên và địa chỉ mẫu không được chứa ký tự ` , ' \"\nvà phải từ 1 đến 106 ký tự", + "error_text_wallet_name": "Tên ví chỉ được chứa chữ cái, số, ký hiệu _ -\nvà phải từ 1 đến 33 ký tự", + "error_text_xmr": "Giá trị XMR không được vượt quá số dư khả dụng.\nSố chữ số thập phân phải ít hơn hoặc bằng 12", + "errorGettingCredentials": "Không thành công: Lỗi khi nhận thông tin xác thực", + "errorSigningTransaction": "Đã xảy ra lỗi khi ký giao dịch", + "estimated": "Ước tính", + "estimated_new_fee": "Phí mới ước tính", + "estimated_receive_amount": "Số tiền nhận ước tính", + "etherscan_history": "Lịch sử Etherscan", + "event": "Sự kiện", + "events": "Các sự kiện", + "exchange": "Trao đổi", + "exchange_incorrect_current_wallet_for_xmr": "Nếu bạn muốn trao đổi XMR từ số dư Monero của Ví Cake, vui lòng chuyển sang ví Monero của bạn trước.", + "exchange_new_template": "Mẫu mới", + "exchange_provider_unsupported": "${providerName} không còn được hỗ trợ nữa!", + "exchange_result_confirm": "Bằng cách nhấn xác nhận, bạn sẽ gửi ${fetchingLabel} ${from} từ ví có tên ${walletName} của mình đến địa chỉ dưới đây. Hoặc bạn có thể gửi từ ví bên ngoài của mình đến địa chỉ/mã QR bên dưới.\n\nVui lòng nhấn xác nhận để tiếp tục hoặc quay lại để thay đổi số tiền.", + "exchange_result_description": "Bạn phải gửi tối thiểu ${fetchingLabel} ${from} đến địa chỉ hiển thị trên trang tiếp theo. Nếu bạn gửi số tiền thấp hơn ${fetchingLabel} ${from} thì có thể không được chuyển đổi và không được hoàn trả.", + "exchange_result_write_down_ID": "*Vui lòng sao chép hoặc ghi lại ID hiển thị ở trên.", + "exchange_result_write_down_trade_id": "Vui lòng sao chép hoặc ghi lại ID giao dịch để tiếp tục.", + "exchange_sync_alert_content": "Vui lòng chờ cho đến khi ví của bạn được đồng bộ hóa", + "expired": "Đã hết hạn", + "expires": "Hết hạn", + "expiresOn": "Hết hạn vào", + "expiry_and_validity": "Hạn và hiệu lực", + "export_backup": "Xuất sao lưu", + "extra_id": "ID bổ sung:", + "extracted_address_content": "Bạn sẽ gửi tiền cho\n${recipient_name}", + "failed_authentication": "Xác thực không thành công. ${state_error}", + "faq": "FAQ", + "features": "Tính năng", + "fetching": "Đang tải", + "fiat_api": "API Fiat", + "fiat_balance": "Số dư Fiat", + "field_required": "Trường này là bắt buộc", + "fill_code": "Vui lòng điền mã xác minh được gửi đến email của bạn", + "filter_by": "Lọc theo", + "first_wallet_text": "Ví tuyệt vời cho Monero, Bitcoin, Ethereum, Litecoin, và Haven", + "fixed_pair_not_supported": "Cặp tỷ giá cố định này không được hỗ trợ với các sàn giao dịch đã chọn", + "fixed_rate": "Tỷ giá cố định", + "fixed_rate_alert": "Bạn sẽ có thể nhập số lượng nhận được khi chế độ tỷ giá cố định được chọn. Bạn có muốn chuyển sang chế độ tỷ giá cố định không?", + "forgot_password": "Quên mật khẩu", + "freeze": "Đóng băng", + "frequently_asked_questions": "Các câu hỏi thường gặp", + "frozen": "Đã đóng băng", + "full_balance": "Số dư đầy đủ", + "generate_name": "Tạo tên", + "generating_gift_card": "Đang tạo thẻ quà tặng", + "get_a": "Nhận một ", + "get_card_note": " mà bạn có thể nạp lại bằng tiền điện tử. Không cần thêm thông tin!", + "get_your_yat": "Nhận Yat của bạn", + "gift_card_amount": "Số tiền thẻ quà tặng", + "gift_card_balance_note": "Các thẻ quà tặng còn số dư sẽ xuất hiện ở đây", + "gift_card_is_generated": "Thẻ quà tặng đã được tạo", + "gift_card_number": "Số thẻ quà tặng", + "gift_card_redeemed_note": "Thẻ quà tặng bạn đã quy đổi sẽ xuất hiện ở đây", + "gift_cards": "Thẻ quà tặng", + "gift_cards_unavailable": "Thẻ quà tặng chỉ có thể mua bằng Monero, Bitcoin, và Litecoin vào lúc này", + "got_it": "Đã hiểu", + "gross_balance": "Số dư gộp", + "group_by_type": "Nhóm theo loại", + "haven_app": "Haven bởi Cake Wallet", + "haven_app_wallet_text": "Ví tuyệt vời cho Haven", + "help": "Trợ giúp", + "hidden_balance": "Số dư ẩn", + "hide_details": "Ẩn chi tiết", + "high_contrast_theme": "Chủ đề độ tương phản cao", + "home_screen_settings": "Cài đặt màn hình chính", + "how_to_use": "Cách sử dụng", + "how_to_use_card": "Cách sử dụng thẻ này", + "id": "ID: ", + "ignor": "Bỏ qua", + "import": "Nhập", + "importNFTs": "Nhập NFT", + "in_store": "Tại cửa hàng", + "incoming": "Đang nhận", + "incorrect_seed": "Văn bản nhập không hợp lệ.", + "inputs": "Đầu vào", + "insufficient_lamport_for_tx": "Bạn không có đủ SOL để thanh toán giao dịch và phí giao dịch. Vui lòng thêm SOL vào ví của bạn hoặc giảm số lượng SOL bạn đang gửi.", + "insufficient_lamports": "Bạn không có đủ SOL để thanh toán giao dịch và phí giao dịch. Bạn cần ít nhất ${solValueNeeded} SOL. Vui lòng thêm SOL vào ví của bạn hoặc giảm số lượng SOL bạn đang gửi", + "insufficientFundsForRentError": "Bạn không có đủ SOL để thanh toán phí giao dịch và phí thuê cho tài khoản. Vui lòng thêm SOL vào ví của bạn hoặc giảm số lượng SOL bạn đang gửi", + "introducing_cake_pay": "Giới thiệu Cake Pay!", + "invalid_input": "Nhập không hợp lệ", + "invalid_password": "Mật khẩu không hợp lệ", + "invoice_details": "Chi tiết hóa đơn", + "is_percentage": "là", + "last_30_days": "30 ngày gần nhất", + "learn_more": "Tìm hiểu thêm", + "ledger_connection_error": "Không thể kết nối với Ledger của bạn. Vui lòng thử lại.", + "ledger_error_device_locked": "Ledger đã bị khóa", + "ledger_error_tx_rejected_by_user": "Giao dịch bị từ chối trên thiết bị", + "ledger_error_wrong_app": "Vui lòng đảm bảo bạn đã mở đúng ứng dụng trên Ledger của mình", + "ledger_please_enable_bluetooth": "Vui lòng bật Bluetooth để phát hiện Ledger của bạn", + "light_theme": "Chủ đề sáng", + "live_fee_rates": "Tỷ lệ phí hiện tại qua API", + "load_more": "Tải thêm", + "loading_your_wallet": "Đang tải ví của bạn", + "login": "Đăng nhập", + "logout": "Đăng xuất", + "low_fee": "Phí thấp", + "low_fee_alert": "Bạn hiện đang sử dụng mức ưu tiên phí mạng thấp. Điều này có thể dẫn đến thời gian chờ lâu, tỷ giá khác nhau, hoặc giao dịch bị hủy bỏ. Chúng tôi khuyến nghị bạn đặt mức phí cao hơn để có trải nghiệm tốt hơn.", + "manage_nodes": "Quản lý các nút", + "manage_pow_nodes": "Quản lý các nút PoW", + "manage_yats": "Quản lý Yats", + "mark_as_redeemed": "Đánh dấu là đã đổi", + "market_place": "Thị trường", + "matrix_green_dark_theme": "Chủ đề tối Matrix Green", + "max_amount": "Tối đa: ${value}", + "max_value": "Tối đa: ${value} ${currency}", + "memo": "Ghi chú:", + "message": "Tin nhắn", + "message_verified": "Tin nhắn đã được xác minh thành công", + "methods": "Phương pháp", + "min_amount": "Tối thiểu: ${value}", + "min_value": "Tối thiểu: ${value} ${currency}", + "minutes_to_pin_code": "${minute} phút", + "mm": "MM", + "modify_2fa": "Chỉnh sửa Cake 2FA", + "monero_com": "Monero.com bởi Cake Wallet", + "monero_com_wallet_text": "Ví tuyệt vời cho Monero", + "monero_dark_theme": "Chủ đề tối Monero", + "monero_light_theme": "Chủ đề sáng Monero", + "moonpay_alert_text": "Giá trị số tiền phải lớn hơn hoặc bằng ${minAmount} ${fiatCurrency}", + "more_options": "Thêm tùy chọn", + "name": "Tên", + "nano_current_rep": "Đại diện hiện tại", + "nano_gpt_thanks_message": "Cảm ơn bạn đã sử dụng NanoGPT! Hãy nhớ quay lại trình duyệt sau khi giao dịch của bạn hoàn tất!", + "nano_pick_new_rep": "Chọn đại diện mới", + "nanogpt_subtitle": "Tất cả các mẫu mới nhất (GPT-4, Claude).\\nKhông cần đăng ký, thanh toán bằng tiền điện tử.", + "narrow": "Hẹp", + "new_first_wallet_text": "Giữ an toàn cho tiền điện tử của bạn, dễ dàng như ăn bánh", + "new_node_testing": "Đang thử nghiệm nút mới", + "new_subaddress_create": "Tạo", + "new_subaddress_label_name": "Tên nhãn", + "new_subaddress_title": "Địa chỉ mới", + "new_template": "Mẫu mới", + "new_wallet": "Ví mới", + "newConnection": "Kết nối mới", + "no_cards_found": "Không tìm thấy thẻ", + "no_id_needed": "Không cần ID!", + "no_id_required": "Không yêu cầu ID. Nạp tiền và chi tiêu ở bất kỳ đâu", + "no_relay_on_domain": "Không có relay cho miền của người dùng hoặc relay không khả dụng. Vui lòng chọn một relay để sử dụng.", + "no_relays": "Không có relay", + "no_relays_message": "Chúng tôi đã tìm thấy một bản ghi Nostr NIP-05 cho người dùng này, nhưng nó không chứa bất kỳ relay nào. Vui lòng hướng dẫn người nhận thêm relay vào bản ghi Nostr của họ.", + "node_address": "Địa chỉ nút", + "node_connection_failed": "Kết nối không thành công", + "node_connection_successful": "Kết nối thành công", + "node_new": "Nút mới", + "node_port": "Cổng nút", + "node_reset_settings_title": "Đặt lại cài đặt", + "node_test": "Kiểm tra", + "nodes": "Các nút", + "nodes_list_reset_to_default_message": "Bạn có chắc chắn muốn đặt lại cài đặt về mặc định không?", + "none_of_selected_providers_can_exchange": "Không có nhà cung cấp nào đã chọn có thể thực hiện giao dịch này", + "noNFTYet": "Chưa có NFT", + "normal": "Bình thường", + "note_optional": "Ghi chú (tùy chọn)", + "note_tap_to_change": "Ghi chú (nhấn để thay đổi)", + "nullURIError": "URI là null", + "offer_expires_in": "Ưu đãi hết hạn trong: ", + "offline": "Ngoại tuyến", + "ok": "OK", + "old_fee": "Phí cũ", + "onion_link": "Liên kết Onion", + "online": "Trực tuyến", + "onramper_option_description": "Mua tiền điện tử nhanh chóng với nhiều phương thức thanh toán. Có sẵn ở hầu hết các quốc gia. Chênh lệch và phí thay đổi.", + "open_gift_card": "Mở thẻ quà tặng", + "optional_description": "Mô tả tùy chọn", + "optional_email_hint": "Email thông báo cho người nhận (tùy chọn)", + "optional_name": "Tên người nhận (tùy chọn)", + "optionally_order_card": "Có thể đặt thẻ vật lý.", + "orbot_running_alert": "Vui lòng đảm bảo Orbot đang chạy trước khi kết nối với nút này.", + "order_by": "Sắp xếp theo", + "order_id": "ID đơn hàng", + "order_physical_card": "Đặt thẻ vật lý", + "other_settings": "Cài đặt khác", + "outdated_electrum_wallet_description": "Ví Bitcoin mới được tạo trong Cake hiện có hạt giống 24 từ. Bạn cần tạo một ví Bitcoin mới và chuyển tất cả số tiền của bạn vào ví 24 từ mới, và ngừng sử dụng ví có hạt giống 12 từ. Vui lòng thực hiện ngay để bảo vệ tài sản của bạn.", + "outdated_electrum_wallet_receive_warning": "Nếu ví này có hạt giống 12 từ và được tạo trong Cake, ĐỪNG gửi Bitcoin vào ví này. Bất kỳ BTC nào chuyển vào ví này có thể bị mất. Tạo ví 24 từ mới (nhấn menu ở góc trên bên phải, chọn Ví, chọn Tạo Ví Mới, sau đó chọn Bitcoin) và NGAY LẬP TỨC chuyển BTC của bạn vào đó. Ví BTC mới (24 từ) từ Cake là an toàn", + "outgoing": "Đang gửi", + "outputs": "Đầu ra", + "overwrite_amount": "Ghi đè số tiền", + "pairingInvalidEvent": "Sự kiện ghép nối không hợp lệ", + "passphrase": "Cụm từ bảo mật (Tùy chọn)", + "password": "Mật khẩu", + "paste": "Dán", + "pause_wallet_creation": "Khả năng tạo ví Haven hiện đang bị tạm dừng.", + "payment_id": "ID thanh toán: ", + "payment_was_received": "Thanh toán của bạn đã được nhận.", + "pending": " (đang chờ)", + "percentageOf": "của ${amount}", + "pin_at_top": "Ghim ${token} ở trên cùng", + "pin_is_incorrect": "PIN không chính xác", + "pin_number": "Số PIN", + "placeholder_contacts": "Danh bạ của bạn sẽ được hiển thị ở đây", + "placeholder_transactions": "Giao dịch của bạn sẽ được hiển thị ở đây", + "please_fill_totp": "Vui lòng điền mã 8 chữ số trên thiết bị khác của bạn", + "please_make_selection": "Vui lòng chọn một tùy chọn dưới đây để tạo hoặc khôi phục ví của bạn.", + "please_reference_document": "Vui lòng tham khảo các tài liệu dưới đây để biết thêm thông tin.", + "please_select": "Vui lòng chọn:", + "please_select_backup_file": "Vui lòng chọn tệp sao lưu và nhập mật khẩu sao lưu.", + "please_try_to_connect_to_another_node": "Vui lòng thử kết nối với một nút khác", + "please_wait": "Vui lòng chờ", + "polygonscan_history": "Lịch sử PolygonScan", + "powered_by": "Được cung cấp bởi ${title}", + "pre_seed_button_text": "Tôi hiểu. Hiển thị hạt giống của tôi", + "pre_seed_description": "Trên trang tiếp theo, bạn sẽ thấy một chuỗi ${words} từ. Đây là hạt giống riêng tư và duy nhất của bạn và là CÁCH DUY NHẤT để khôi phục ví của bạn trong trường hợp mất hoặc hỏng hóc. Đây là TRÁCH NHIỆM của bạn để ghi lại và lưu trữ nó ở một nơi an toàn ngoài ứng dụng Cake Wallet.", + "pre_seed_title": "QUAN TRỌNG", + "prepaid_cards": "Thẻ trả trước", + "prevent_screenshots": "Ngăn chặn ảnh chụp màn hình và ghi hình màn hình", + "privacy": "Quyền riêng tư", + "privacy_policy": "Chính sách quyền riêng tư", + "privacy_settings": "Cài đặt quyền riêng tư", + "private_key": "Khóa riêng", + "proceed_after_one_minute": "Nếu màn hình không tiếp tục sau 1 phút, hãy kiểm tra email của bạn.", + "proceed_on_device": "Tiếp tục trên thiết bị của bạn", + "proceed_on_device_description": "Vui lòng làm theo các hướng dẫn được nhắc trên ví phần cứng của bạn", + "profile": "Hồ sơ", + "provider_error": "Lỗi ${provider}", + "public_key": "Khóa công khai", + "purchase_gift_card": "Mua thẻ quà tặng", + "purple_dark_theme": "Chủ đề tối tím", + "qr_fullscreen": "Nhấn để mở mã QR toàn màn hình", + "qr_payment_amount": "Mã QR này chứa số tiền thanh toán. Bạn có muốn ghi đè giá trị hiện tại không?", + "quantity": "Số lượng", + "question_to_disable_2fa": "Bạn có chắc chắn muốn tắt Cake 2FA không? Mã 2FA sẽ không còn cần thiết để truy cập ví và một số chức năng nhất định.", + "receivable_balance": "Số dư có thể nhận", + "receive": "Nhận", + "receive_amount": "Số lượng", + "received": "Đã nhận", + "recipient_address": "Địa chỉ người nhận", + "reconnect": "Kết nối lại", + "reconnect_alert_text": "Bạn có chắc chắn muốn kết nối lại không?", + "reconnection": "Kết nối lại", + "red_dark_theme": "Chủ đề tối đỏ", + "red_light_theme": "Chủ đề sáng đỏ", + "redeemed": "Đã đổi", + "refund_address": "Địa chỉ hoàn tiền", + "reject": "Từ chối", + "remaining": "còn lại", + "remove": "Gỡ bỏ", + "remove_node": "Gỡ bỏ nút", + "remove_node_message": "Bạn có chắc chắn muốn gỡ bỏ nút đã chọn không?", + "rename": "Đổi tên", + "rep_warning": "Cảnh báo đại diện", + "rep_warning_sub": "Đại diện của bạn dường như không còn trong tình trạng tốt. Nhấn vào đây để chọn một cái mới", + "repeat_wallet_password": "Nhập lại mật khẩu ví", + "repeated_password_is_incorrect": "Mật khẩu nhập lại không chính xác. Vui lòng nhập lại mật khẩu ví.", + "require_for_adding_contacts": "Yêu cầu khi thêm danh bạ", + "require_for_all_security_and_backup_settings": "Yêu cầu cho tất cả các cài đặt bảo mật và sao lưu", + "require_for_assessing_wallet": "Yêu cầu khi truy cập ví", + "require_for_creating_new_wallets": "Yêu cầu khi tạo ví mới", + "require_for_exchanges_to_external_wallets": "Yêu cầu khi đổi sang ví ngoài", + "require_for_exchanges_to_internal_wallets": "Yêu cầu khi đổi sang ví nội bộ", + "require_for_sends_to_contacts": "Yêu cầu khi gửi đến danh bạ", + "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", + "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_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", + "restore_bitcoin_description_from_seed": "Khôi phục ví của bạn từ mã kết hợp 24 từ", + "restore_bitcoin_title_from_keys": "Khôi phục từ WIF", + "restore_description_from_backup": "Bạn có thể khôi phục toàn bộ ứng dụng Cake Wallet từ tệp sao lưu của bạn", + "restore_description_from_hardware_wallet": "Khôi phục từ ví phần cứng Ledger", + "restore_description_from_keys": "Khôi phục ví của bạn từ các thao tác nhập được lưu từ khóa riêng của bạn", + "restore_description_from_seed": "Khôi phục ví của bạn từ mã kết hợp 25 từ hoặc 13 từ", + "restore_description_from_seed_keys": "Khôi phục ví của bạn từ hạt giống/khóa mà bạn đã lưu ở nơi an toàn", + "restore_from_date_or_blockheight": "Vui lòng nhập một ngày vài ngày trước khi bạn tạo ví này. Hoặc nếu bạn biết chiều cao khối, hãy nhập nó thay thế", + "restore_from_seed_placeholder": "Vui lòng nhập hoặc dán hạt giống của bạn vào đây", + "restore_new_seed": "Hạt giống mới", + "restore_next": "Tiếp theo", + "restore_recover": "Khôi phục", + "restore_restore_wallet": "Khôi phục Ví", + "restore_seed_keys_restore": "Khôi phục hạt giống/khóa", + "restore_spend_key_private": "Khóa chi tiêu (riêng tư)", + "restore_title_from_backup": "Khôi phục từ sao lưu", + "restore_title_from_hardware_wallet": "Khôi phục từ ví phần cứng", + "restore_title_from_keys": "Khôi phục từ khóa", + "restore_title_from_seed": "Khôi phục từ hạt giống", + "restore_title_from_seed_keys": "Khôi phục từ hạt giống/khóa", + "restore_view_key_private": "Khóa xem (riêng tư)", + "restore_wallet": "Khôi phục Ví", + "restore_wallet_name": "Tên ví", + "restore_wallet_restore_description": "Mô tả khôi phục ví", + "robinhood_option_description": "Mua và chuyển ngay lập tức bằng thẻ ghi nợ, tài khoản ngân hàng hoặc số dư Robinhood của bạn. Chỉ có ở Mỹ.", + "router_no_route": "Không có tuyến đường nào được định nghĩa cho ${name}", + "save": "Lưu", + "save_backup_password": "Vui lòng đảm bảo rằng bạn đã lưu mật khẩu sao lưu của mình. Bạn sẽ không thể nhập tệp sao lưu của mình nếu không có nó.", + "save_backup_password_alert": "Lưu mật khẩu sao lưu", + "save_to_downloads": "Lưu vào Tải xuống", + "saved_the_trade_id": "Tôi đã lưu ID giao dịch", + "scan_one_block": "Quét một khối", + "scan_qr_code": "Quét mã QR", + "scan_qr_code_to_get_address": "Quét mã QR để nhận địa chỉ", + "scan_qr_on_device": "Quét mã QR này trên thiết bị khác", + "search": "Tìm kiếm", + "search_add_token": "Tìm kiếm / Thêm token", + "search_category": "Tìm kiếm danh mục", + "search_currency": "Tìm kiếm tiền tệ", + "search_language": "Tìm kiếm ngôn ngữ", + "second_intro_content": "Yat của bạn là một địa chỉ emoji duy nhất thay thế tất cả các địa chỉ hex dài của bạn cho tất cả các loại tiền tệ của bạn.", + "second_intro_title": "Một địa chỉ emoji để cai trị tất cả", + "security_and_backup": "Bảo mật và sao lưu", + "seed_alert_back": "Quay lại", + "seed_alert_content": "Hạt giống là cách duy nhất để khôi phục ví của bạn. Bạn đã ghi lại nó chưa?", + "seed_alert_title": "Chú ý", + "seed_alert_yes": "Có, tôi đã ghi lại", + "seed_choose": "Chọn ngôn ngữ hạt giống", + "seed_hex_form": "Hạt giống ví (dạng hex)", + "seed_key": "Khóa hạt giống", + "seed_language": "Ngôn ngữ hạt giống", + "seed_language_chinese": "Tiếng Trung", + "seed_language_chinese_traditional": "Tiếng Trung (Truyền thống)", + "seed_language_czech": "Tiếng Séc", + "seed_language_dutch": "Tiếng Hà Lan", + "seed_language_english": "Tiếng Anh", + "seed_language_french": "Tiếng Pháp", + "seed_language_german": "Tiếng Đức", + "seed_language_italian": "Tiếng Ý", + "seed_language_japanese": "Tiếng Nhật", + "seed_language_korean": "Tiếng Hàn", + "seed_language_next": "Tiếp theo", + "seed_language_portuguese": "Tiếng Bồ Đào Nha", + "seed_language_russian": "Tiếng Nga", + "seed_language_spanish": "Tiếng Tây Ban Nha", + "seed_phrase_length": "Độ dài cụm từ hạt giống", + "seed_reminder": "Vui lòng ghi lại những điều này phòng khi bạn mất hoặc xóa điện thoại của mình", + "seed_share": "Chia sẻ hạt giống", + "seed_title": "Hạt giống", + "seedtype": "Loại hạt giống", + "seedtype_legacy": "Di sản (25 từ)", + "seedtype_polyseed": "Polyseed (16 từ)", + "seedtype_wownero": "Wownero (14 từ)", + "select_backup_file": "Chọn tệp sao lưu", + "select_buy_provider_notice": "Chọn nhà cung cấp mua ở trên. Bạn có thể bỏ qua màn hình này bằng cách thiết lập nhà cung cấp mua mặc định trong cài đặt ứng dụng.", + "select_destination": "Vui lòng chọn đích cho tệp sao lưu.", + "select_sell_provider_notice": "Chọn nhà cung cấp bán ở trên. Bạn có thể bỏ qua màn hình này bằng cách thiết lập nhà cung cấp bán mặc định trong cài đặt ứng dụng.", + "sell": "Bán", + "sell_alert_content": "Hiện tại chúng tôi chỉ hỗ trợ bán Bitcoin, Ethereum và Litecoin. Vui lòng tạo hoặc chuyển sang ví Bitcoin, Ethereum hoặc Litecoin của bạn.", + "sell_monero_com_alert_content": "Bán Monero chưa được hỗ trợ", + "send": "Gửi", + "send_address": "Địa chỉ ${cryptoCurrency}", + "send_amount": "Số tiền:", + "send_creating_transaction": "Tạo giao dịch", + "send_error_currency": "Tiền tệ chỉ có thể chứa số", + "send_error_minimum_value": "Giá trị tối thiểu của số tiền là 0.01", + "send_estimated_fee": "Phí ước lượng:", + "send_fee": "Phí:", + "send_name": "Tên", + "send_new": "Mới", + "send_payment_id": "ID thanh toán (tùy chọn)", + "send_priority": "Hiện tại phí được đặt ở mức ưu tiên ${transactionPriority}.\nƯu tiên giao dịch có thể được điều chỉnh trong cài đặt", + "send_sending": "Đang gửi...", + "send_success": "Đã gửi ${crypto} của bạn thành công", + "send_templates": "Mẫu", + "send_title": "Gửi", + "send_to_this_address": "Gửi ${currency} ${tag} đến địa chỉ này", + "send_xmr": "Gửi XMR", + "send_your_wallet": "Ví của bạn", + "sending": "Đang gửi", + "sent": "Đã gửi", + "service_health_disabled": "Thông báo sức khỏe dịch vụ bị vô hiệu hóa", + "service_health_disabled_message": "Đây là trang thông báo sức khỏe dịch vụ, bạn có thể kích hoạt trang này trong Cài đặt -> Quyền riêng tư", + "settings": "Cài đặt", + "settings_all": "TẤT CẢ", + "settings_allow_biometrical_authentication": "Cho phép xác thực sinh trắc học", + "settings_can_be_changed_later": "Các cài đặt này có thể được thay đổi sau trong cài đặt ứng dụng", + "settings_change_language": "Thay đổi ngôn ngữ", + "settings_change_pin": "Thay đổi PIN", + "settings_currency": "Tiền tệ", + "settings_current_node": "Nút hiện tại", + "settings_dark_mode": "Chế độ tối", + "settings_display_balance": "Hiển thị số dư", + "settings_display_on_dashboard_list": "Hiển thị trên danh sách bảng điều khiển", + "settings_fee_priority": "Ưu tiên phí", + "settings_nodes": "Các nút", + "settings_none": "Không có", + "settings_only_trades": "Chỉ giao dịch", + "settings_only_transactions": "Chỉ giao dịch", + "settings_personal": "Cá nhân", + "settings_save_recipient_address": "Lưu địa chỉ người nhận", + "settings_support": "Hỗ trợ", + "settings_terms_and_conditions": "Điều khoản và Điều kiện", + "settings_title": "Cài đặt", + "settings_trades": "Giao dịch", + "settings_transactions": "Giao dịch", + "settings_wallets": "Ví", + "setup_2fa": "Thiết lập Cake 2FA", + "setup_2fa_text": "Cake 2FA hoạt động bằng cách sử dụng TOTP làm yếu tố xác thực thứ hai.\n\nTOTP của Cake 2FA yêu cầu hỗ trợ SHA-512 và 8 chữ số; điều này cung cấp bảo mật cao hơn. Thông tin thêm và các ứng dụng hỗ trợ có thể được tìm thấy trong hướng dẫn.", + "setup_pin": "Thiết lập PIN", + "setup_successful": "PIN của bạn đã được thiết lập thành công!", + "setup_totp_recommended": "Thiết lập TOTP", + "setup_warning_2fa_text": "Cake 2FA là xác thực thứ hai cho một số hành động trong ví. Nó KHÔNG an toàn như lưu trữ lạnh.\n\nNếu bạn mất quyền truy cập vào ứng dụng 2FA hoặc các khóa TOTP, bạn SẼ mất quyền truy cập vào ví này. Bạn sẽ cần phải khôi phục ví của bạn từ hạt giống nhớ.\n\nHỗ trợ Cake sẽ không thể hỗ trợ bạn nếu bạn mất quyền truy cập vào 2FA hoặc hạt giống nhớ của bạn.\nTrước khi sử dụng Cake 2FA, chúng tôi khuyến nghị đọc kỹ hướng dẫn.", + "setup_your_debit_card": "Thiết lập thẻ ghi nợ của bạn", + "share": "Chia sẻ", + "share_address": "Chia sẻ địa chỉ", + "show_details": "Hiển thị chi tiết", + "show_keys": "Hiển thị hạt giống/khóa", + "show_market_place": "Hiển thị Thị trường", + "show_seed": "Hiển thị hạt giống", + "sign_message": "Ký tin nhắn", + "sign_up": "Đăng ký", + "sign_verify_message": "Ký hoặc xác minh tin nhắn", + "sign_verify_message_sub": "Ký hoặc xác minh một tin nhắn bằng khóa riêng của bạn", + "sign_verify_title": "Ký / Xác minh", + "signature": "Chữ ký", + "signature_invalid_error": "Chữ ký không hợp lệ cho tin nhắn đã cho", + "signTransaction": "Ký giao dịch", + "signup_for_card_accept_terms": "Đăng ký thẻ và chấp nhận các điều khoản.", + "silent_payments": "Thanh toán im lặng", + "silent_payments_always_scan": "Đặt Thanh toán im lặng luôn quét", + "silent_payments_disclaimer": "Địa chỉ mới không phải là danh tính mới. Đây là việc tái sử dụng một danh tính hiện có với nhãn khác.", + "silent_payments_display_card": "Hiển thị thẻ Thanh toán im lặng", + "silent_payments_scan_from_date": "Quét từ ngày", + "silent_payments_scan_from_date_or_blockheight": "Vui lòng nhập chiều cao khối bạn muốn bắt đầu quét cho các thanh toán im lặng đến, hoặc, sử dụng ngày thay thế. Bạn có thể chọn nếu ví tiếp tục quét mỗi khối, hoặc chỉ kiểm tra chiều cao đã chỉ định.", + "silent_payments_scan_from_height": "Quét từ chiều cao khối", + "silent_payments_scanned_tip": "ĐÃ QUÉT ĐỂ TIP! (${tip})", + "silent_payments_scanning": "Đang quét thanh toán im lặng", + "silent_payments_settings": "Cài đặt thanh toán im lặng", + "slidable": "Có thể kéo", + "sort_by": "Sắp xếp theo", + "spend_key_private": "Khóa chi tiêu (riêng tư)", + "spend_key_public": "Khóa chi tiêu (công khai)", + "status": "Trạng thái: ", + "string_default": "Mặc định", + "subaddress_title": "Danh sách địa chỉ phụ", + "subaddresses": "Địa chỉ phụ", + "submit_request": "gửi yêu cầu", + "successful": "Thành công", + "support_description_guides": "Tài liệu và hỗ trợ cho các vấn đề phổ biến", + "support_description_live_chat": "Miễn phí và nhanh chóng! Các đại diện hỗ trợ được đào tạo sẵn sàng hỗ trợ", + "support_description_other_links": "Tham gia cộng đồng của chúng tôi hoặc liên hệ với chúng tôi hoặc các đối tác của chúng tôi qua các phương pháp khác", + "support_title_guides": "Hướng dẫn Cake Wallet", + "support_title_live_chat": "Hỗ trợ trực tiếp", + "support_title_other_links": "Liên kết hỗ trợ khác", + "sweeping_wallet": "Quét ví", + "sweeping_wallet_alert": "Việc này không nên mất nhiều thời gian. KHÔNG RỜI KHỎI MÀN HÌNH NÀY HOẶC CÁC KHOẢN TIỀN ĐƯỢC QUÉT CÓ THỂ BỊ MẤT.", + "switchToETHWallet": "Vui lòng chuyển sang ví Ethereum và thử lại", + "switchToEVMCompatibleWallet": "Vui lòng chuyển sang ví tương thích EVM và thử lại (Ethereum, Polygon)", + "symbol": "Ký hiệu", + "sync_all_wallets": "Đồng bộ tất cả các ví", + "sync_status_attempting_sync": "ĐANG THỬ ĐỒNG BỘ", + "sync_status_connected": "ĐÃ KẾT NỐI", + "sync_status_connecting": "ĐANG KẾT NỐI", + "sync_status_failed_connect": "ĐÃ NGẮT KẾT NỐI", + "sync_status_not_connected": "CHƯA KẾT NỐI", + "sync_status_starting_scan": "ĐANG BẮT ĐẦU QUÉT", + "sync_status_starting_sync": "ĐANG BẮT ĐẦU ĐỒNG BỘ", + "sync_status_syncronized": "ĐÃ ĐỒNG BỘ", + "sync_status_syncronizing": "ĐANG ĐỒNG BỘ", + "sync_status_timed_out": "HẾT THỜI GIAN", + "sync_status_unsupported": "NÓT KHÔNG ĐƯỢC HỖ TRỢ", + "syncing_wallet_alert_content": "Số dư và danh sách giao dịch của bạn có thể không đầy đủ cho đến khi nó hiển thị “ĐÃ ĐỒNG BỘ” ở trên cùng. Nhấn vào đây để tìm hiểu thêm.", + "syncing_wallet_alert_title": "Ví của bạn đang đồng bộ", + "template": "Mẫu", + "template_name": "Tên mẫu", + "testnet_coins_no_value": "Tiền tệ testnet không có giá trị", + "third_intro_content": "Yats cũng tồn tại ngoài Cake Wallet. Bất kỳ địa chỉ ví nào trên thế giới đều có thể được thay thế bằng một Yat!", + "third_intro_title": "Yat tương thích tốt với các đối tượng khác", + "thorchain_contract_address_not_supported": "THORChain không hỗ trợ gửi đến địa chỉ hợp đồng", + "thorchain_taproot_address_not_supported": "Nhà cung cấp ThorChain không hỗ trợ địa chỉ Taproot. Vui lòng thay đổi địa chỉ hoặc chọn nhà cung cấp khác.", + "time": "${minutes} phút ${seconds} giây", + "tip": "Mẹo:", + "today": "Hôm nay", + "token_contract_address": "Địa chỉ hợp đồng token", + "token_decimal": "Số thập phân của token", + "token_name": "Tên token ví dụ: Tether", + "token_symbol": "Ký hiệu token ví dụ: USDT", + "tokenID": "ID", + "tor_connection": "Kết nối Tor", + "tor_only": "Chỉ Tor", + "total": "Tổng cộng", + "total_saving": "Tiết kiệm tổng cộng", + "totp_2fa_failure": "Mã không chính xác. Vui lòng thử mã khác hoặc tạo khóa bí mật mới. Sử dụng ứng dụng 2FA tương thích hỗ trợ mã 8 chữ số và SHA512.", + "totp_2fa_success": "Thành công! Cake 2FA đã được kích hoạt cho ví này. Hãy nhớ lưu hạt giống nhớ của bạn phòng trường hợp bạn mất quyền truy cập vào ví.", + "totp_auth_url": "URL XÁC THỰC TOTP", + "totp_code": "Mã TOTP", + "totp_secret_code": "Mã bí mật TOTP", + "totp_verification_success": "Xác minh thành công!", + "track": "Theo dõi", + "trade_details_copied": "${title} đã được sao chép vào clipboard", + "trade_details_created_at": "Tạo lúc", + "trade_details_fetching": "Đang lấy dữ liệu", + "trade_details_id": "ID", + "trade_details_pair": "Cặp", + "trade_details_provider": "Nhà cung cấp", + "trade_details_state": "Trạng thái", + "trade_details_title": "Chi tiết giao dịch", + "trade_for_not_created": "Giao dịch cho ${title} chưa được tạo.", + "trade_history_title": "Lịch sử giao dịch", + "trade_id": "ID giao dịch:", + "trade_id_not_found": "Giao dịch ${tradeId} của ${title} không tìm thấy.", + "trade_is_powered_by": "Giao dịch này được cung cấp bởi ${provider}", + "trade_not_created": "Giao dịch chưa được tạo", + "trade_not_found": "Giao dịch không tìm thấy.", + "trade_state_btc_sent": "BTC đã gửi", + "trade_state_complete": "Hoàn thành", + "trade_state_confirming": "Đang xác nhận", + "trade_state_created": "Đã tạo", + "trade_state_finished": "Đã hoàn tất", + "trade_state_paid": "Đã thanh toán", + "trade_state_paid_unconfirmed": "Đã thanh toán chưa xác nhận", + "trade_state_pending": "Đang chờ xử lý", + "trade_state_timeout": "Hết thời gian", + "trade_state_to_be_created": "Sẽ được tạo", + "trade_state_traded": "Đã giao dịch", + "trade_state_trading": "Đang giao dịch", + "trade_state_underpaid": "Thanh toán chưa đủ", + "trade_state_unpaid": "Chưa thanh toán", + "trades": "Giao dịch", + "transaction_details_amount": "Số tiền", + "transaction_details_copied": "${title} đã được sao chép vào clipboard", + "transaction_details_date": "Ngày", + "transaction_details_fee": "Phí", + "transaction_details_height": "Chiều cao", + "transaction_details_recipient_address": "Địa chỉ người nhận", + "transaction_details_source_address": "Địa chỉ nguồn", + "transaction_details_title": "Chi tiết giao dịch", + "transaction_details_transaction_id": "ID giao dịch", + "transaction_key": "Khóa giao dịch", + "transaction_priority_fast": "Nhanh", + "transaction_priority_fastest": "Nhanh nhất", + "transaction_priority_medium": "Trung bình", + "transaction_priority_regular": "Thông thường", + "transaction_priority_slow": "Chậm", + "transaction_sent": "Giao dịch đã được gửi!", + "transaction_sent_notice": "Nếu màn hình không tiếp tục sau 1 phút, hãy kiểm tra trình khám phá khối và email của bạn.", + "transactions": "Giao dịch", + "transactions_by_date": "Giao dịch theo ngày", + "trongrid_history": "Lịch sử TronGrid", + "trusted": "Đã tin cậy", + "tx_commit_exception_no_dust_on_change": "Giao dịch bị từ chối với số tiền này. Với số tiền này bạn có thể gửi ${min} mà không cần đổi tiền lẻ hoặc ${max} trả lại tiền lẻ.", + "tx_commit_failed": "Giao dịch không thành công. Vui lòng liên hệ với hỗ trợ.", + "tx_invalid_input": "Bạn đang sử dụng loại đầu vào sai cho loại thanh toán này", + "tx_no_dust_exception": "Giao dịch bị từ chối vì gửi một số tiền quá nhỏ. Vui lòng thử tăng số tiền.", + "tx_not_enough_inputs_exception": "Không đủ đầu vào có sẵn. Vui lòng chọn thêm dưới Coin Control", + "tx_rejected_bip68_final": "Giao dịch có đầu vào chưa xác nhận và không thể thay thế bằng phí.", + "tx_rejected_dust_change": "Giao dịch bị từ chối bởi quy tắc mạng, số tiền thay đổi thấp (dust). Thử gửi TOÀN BỘ hoặc giảm số tiền.", + "tx_rejected_dust_output": "Giao dịch bị từ chối bởi quy tắc mạng, số tiền đầu ra thấp (dust). Vui lòng tăng số tiền.", + "tx_rejected_dust_output_send_all": "Giao dịch bị từ chối bởi quy tắc mạng, số tiền đầu ra thấp (dust). Vui lòng kiểm tra số dư của các đồng tiền được chọn dưới Coin Control.", + "tx_rejected_vout_negative": "Không đủ số dư để thanh toán phí giao dịch này. Vui lòng kiểm tra số dư của các đồng tiền dưới Coin Control.", + "tx_wrong_balance_exception": "Bạn không có đủ ${currency} để gửi số tiền này.", + "tx_wrong_balance_with_amount_exception": "Bạn không có đủ ${currency} để gửi tổng số tiền ${amount}", + "tx_zero_fee_exception": "Không thể gửi giao dịch với phí bằng 0. Thử tăng tỷ lệ phí hoặc kiểm tra kết nối của bạn để biết ước lượng mới nhất.", + "unavailable_balance": "Số dư không khả dụng", + "unavailable_balance_description": "Số dư không khả dụng: Tổng số này bao gồm các khoản tiền bị khóa trong các giao dịch chờ xử lý và những khoản bạn đã chủ động đóng băng trong cài đặt điều khiển tiền của bạn. Các số dư bị khóa sẽ trở nên khả dụng khi các giao dịch tương ứng của chúng hoàn tất, trong khi các số dư bị đóng băng vẫn không thể truy cập cho các giao dịch cho đến khi bạn quyết định mở khóa chúng.", + "unconfirmed": "Số dư chưa xác nhận", + "understand": "Tôi hiểu", + "unlock": "Mở khóa", + "unmatched_currencies": "Tiền tệ của ví hiện tại của bạn không khớp với QR đã quét", + "unspent_change": "Tiền thối", + "unspent_coins_details_title": "Chi tiết các đồng tiền chưa chi tiêu", + "unspent_coins_title": "Các đồng tiền chưa chi tiêu", + "unsupported_asset": "Chúng tôi không hỗ trợ hành động này cho tài sản này. Vui lòng tạo hoặc chuyển sang ví của loại tài sản được hỗ trợ.", + "uptime": "Thời gian hoạt động", + "upto": "lên đến ${value}", + "usb": "USB", + "use": "Chuyển sang", + "use_card_info_three": "Sử dụng thẻ kỹ thuật số trực tuyến hoặc với các phương thức thanh toán không tiếp xúc.", + "use_card_info_two": "Các khoản tiền được chuyển đổi thành USD khi chúng được giữ trong tài khoản trả trước, không phải trong các loại tiền kỹ thuật số.", + "use_ssl": "Sử dụng SSL", + "use_suggested": "Sử dụng đề xuất", + "use_testnet": "Sử dụng Testnet", + "value": "Giá trị", + "value_type": "Loại giá trị", + "variable_pair_not_supported": "Cặp biến này không được hỗ trợ với các sàn giao dịch đã chọn", + "verification": "Xác minh", + "verify_message": "Xác minh tin nhắn", + "verify_with_2fa": "Xác minh với Cake 2FA", + "version": "Phiên bản ${currentVersion}", + "view_all": "Xem tất cả", + "view_in_block_explorer": "Xem trong Block Explorer", + "view_key_private": "Xem khóa (riêng tư)", + "view_key_public": "Xem khóa (công khai)", + "view_transaction_on": "Xem giao dịch trên", + "voting_weight": "Trọng số bỏ phiếu", + "waitFewSecondForTxUpdate": "Vui lòng đợi vài giây để giao dịch được phản ánh trong lịch sử giao dịch", + "wallet_keys": "Hạt giống/khóa ví", + "wallet_list_create_new_wallet": "Tạo ví mới", + "wallet_list_edit_wallet": "Chỉnh sửa ví", + "wallet_list_failed_to_load": "Tải ví ${wallet_name} không thành công. ${error}", + "wallet_list_failed_to_remove": "Xóa ví ${wallet_name} không thành công. ${error}", + "wallet_list_load_wallet": "Tải ví", + "wallet_list_loading_wallet": "Đang tải ví ${wallet_name}", + "wallet_list_removing_wallet": "Đang xóa ví ${wallet_name}", + "wallet_list_restore_wallet": "Khôi phục ví", + "wallet_list_title": "Ví Monero", + "wallet_list_wallet_name": "Tên ví", + "wallet_menu": "Menu", + "wallet_name": "Tên ví", + "wallet_name_exists": "Một ví với tên đó đã tồn tại. Vui lòng chọn tên khác hoặc đổi tên ví khác trước.", + "wallet_password_is_empty": "Mật khẩu ví bị bỏ trống. Mật khẩu ví không được để trống", + "wallet_recovery_height": "Chiều cao khôi phục", + "wallet_restoration_store_incorrect_seed_length": "Độ dài hạt giống không chính xác", + "wallet_seed": "Hạt giống ví", + "wallet_seed_legacy": "Hạt giống ví cũ", + "wallet_store_monero_wallet": "Ví Monero", + "walletConnect": "WalletConnect", + "wallets": "Các ví", + "warning": "Cảnh báo", + "welcome": "Chào mừng đến với", + "welcome_to_cakepay": "Chào mừng đến với Cake Pay!", + "what_is_silent_payments": "Thanh toán im lặng là gì?", + "widgets_address": "Địa chỉ", + "widgets_or": "hoặc", + "widgets_restore_from_blockheight": "Khôi phục từ chiều cao khối", + "widgets_restore_from_date": "Khôi phục từ ngày", + "widgets_seed": "Hạt giống", + "wouoldLikeToConnect": "muốn kết nối", + "write_down_backup_password": "Vui lòng ghi lại mật khẩu sao lưu của bạn, được sử dụng để nhập các tệp sao lưu của bạn.", + "xlm_extra_info": "Vui lòng đừng quên chỉ định Memo ID khi gửi giao dịch XLM để trao đổi", + "xmr_available_balance": "Số dư khả dụng", + "xmr_full_balance": "Số dư đầy đủ", + "xmr_hidden": "Bị ẩn", + "xmr_to_error": "Lỗi XMR.TO", + "xmr_to_error_description": "Số tiền không hợp lệ. Giới hạn tối đa 8 chữ số sau dấu thập phân", + "xrp_extra_info": "Vui lòng đừng quên chỉ định Destination Tag khi gửi giao dịch XRP để trao đổi", + "yat": "Yat", + "yat_address": "Địa chỉ Yat", + "yat_alert_content": "Người dùng Cake Wallet hiện có thể gửi và nhận tất cả các loại tiền yêu thích của họ với tên người dùng dựa trên emoji độc đáo.", + "yat_alert_title": "Gửi và nhận crypto dễ dàng hơn với Yat", + "yat_error": "Lỗi Yat", + "yat_error_content": "Không có địa chỉ liên kết với Yat này. Thử Yat khác", + "yat_popup_content": "Bây giờ bạn có thể gửi và nhận crypto trong Cake Wallet với Yat của bạn - một tên người dùng ngắn gọn dựa trên emoji. Quản lý Yats bất cứ lúc nào trên màn hình cài đặt", + "yat_popup_title": "Địa chỉ ví của bạn có thể được chuyển thành emoji.", + "yesterday": "Hôm qua", + "you_now_have_debit_card": "Bạn hiện có một thẻ ghi nợ", + "you_pay": "Bạn thanh toán", + "you_will_get": "Chuyển đổi thành", + "you_will_send": "Chuyển đổi từ", + "yy": "YY" +} diff --git a/tool/utils/translation/translation_constants.dart b/tool/utils/translation/translation_constants.dart index 3d1afe142..3f392c639 100644 --- a/tool/utils/translation/translation_constants.dart +++ b/tool/utils/translation/translation_constants.dart @@ -2,5 +2,5 @@ const defaultLang = "en"; const langs = [ "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "hy", "id", "it", "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tl", "tr", "uk", "ur", "yo", - "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified) + "vi", "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified) ]; From 0e106834bc6c4202fe35675fcfb1257dadf28c93 Mon Sep 17 00:00:00 2001 From: Rafael Date: Wed, 11 Sep 2024 12:54:13 -0300 Subject: [PATCH 077/142] bump dep (#1679) --- cw_bitcoin/pubspec.lock | 12 ++++++------ cw_bitcoin/pubspec.yaml | 2 +- cw_core/pubspec.lock | 4 ++-- cw_core/pubspec.yaml | 2 +- cw_haven/pubspec.lock | 4 ++-- cw_monero/pubspec.lock | 8 ++++---- cw_wownero/pubspec.lock | 8 ++++---- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index b5e59cd88..301b2aeff 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -304,18 +304,18 @@ packages: dependency: transitive description: name: ffigen - sha256: d3e76c2ad48a4e7f93a29a162006f00eba46ce7c08194a77bb5c5e97d1b5ff0a + sha256: "3e12e80ccb6539bb3917217bb6f32709220efb737de0d0fa8736da0b7cb507da" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "12.0.0" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -855,8 +855,8 @@ packages: description: path: "." ref: "sp_v4.0.0" - resolved-ref: "3b8ae38592c0584f53560071dc18bc570758fe13" - url: "https://github.com/rafael-xmr/sp_scanner" + resolved-ref: "9b04f4b0af80dd7dae9274b496a53c23dcc80ea5" + url: "https://github.com/cake-tech/sp_scanner" source: git version: "0.0.1" stack_trace: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 84164b884..d085d98e3 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: url: https://github.com/cake-tech/ledger-bitcoin sp_scanner: git: - url: https://github.com/rafael-xmr/sp_scanner + url: https://github.com/cake-tech/sp_scanner ref: sp_v4.0.0 diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index e905af2d9..c2bdda5f1 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -230,10 +230,10 @@ packages: dependency: "direct main" description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 4497a709d..070779caa 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter http: ^1.1.0 - file: ^6.1.4 + file: ^7.0.0 path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index 6e840224c..1369675f5 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index cf9da9455..360d33ae3 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 - resolved-ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 + ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" + resolved-ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index b68e9ce74..0e97b3214 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 - resolved-ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 + ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" + resolved-ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" From 7bf2fb9dced95e66071cc3f290f2bf2507699906 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Wed, 11 Sep 2024 22:19:47 +0100 Subject: [PATCH 078/142] feat: Add warning before adding erc20 token via contract address (#1663) * feat: Add warning before adding erc20 token via contract address * fix: Update message on edit token page * fix: Adjust polygon secrets variable name * fix: Handle loading state, translate strings and fix other issues * fix linux build [skip ci] * fix: Modify warning text for contract address scam alert * Update secret_key.dart --------- Co-authored-by: Omar Hatem --- .github/workflows/pr_test_build_android.yml | 2 + .github/workflows/pr_test_build_linux.yml | 2 + lib/entities/erc20_token_info_explorers.dart | 80 +++++ lib/entities/erc20_token_info_moralis.dart | 85 +++++ .../screens/dashboard/edit_token_page.dart | 139 +++++--- .../dashboard/home_settings_view_model.dart | 313 +++++++++++++++--- res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 2 + res/values/strings_en.arb | 2 + res/values/strings_es.arb | 2 + res/values/strings_fr.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_hy.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + tool/utils/secret_key.dart | 2 + 34 files changed, 584 insertions(+), 93 deletions(-) create mode 100644 lib/entities/erc20_token_info_explorers.dart create mode 100644 lib/entities/erc20_token_info_moralis.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 1b26d87d0..ac2480cb0 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -149,6 +149,8 @@ jobs: echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index db88d7850..7713cc95d 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -125,6 +125,8 @@ jobs: echo "const sideShiftAffiliateId = '${{ secrets.SIDE_SHIFT_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const simpleSwapApiKey = '${{ secrets.SIMPLE_SWAP_API_KEY }}';" >> lib/.secrets.g.dart echo "const simpleSwapApiKeyDesktop = '${{ secrets.SIMPLE_SWAP_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const onramperApiKey = '${{ secrets.ONRAMPER_API_KEY }}';" >> lib/.secrets.g.dart echo "const anypayToken = '${{ secrets.ANY_PAY_TOKEN }}';" >> lib/.secrets.g.dart echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart diff --git a/lib/entities/erc20_token_info_explorers.dart b/lib/entities/erc20_token_info_explorers.dart new file mode 100644 index 000000000..386db5a50 --- /dev/null +++ b/lib/entities/erc20_token_info_explorers.dart @@ -0,0 +1,80 @@ +class Erc20TokenInfoExplorers { + String? contractAddress; + String? tokenName; + String? symbol; + String? divisor; + String? tokenType; + String? totalSupply; + String? blueCheckmark; + String? description; + String? website; + String? email; + String? blog; + String? reddit; + String? slack; + String? facebook; + String? twitter; + String? bitcointalk; + String? github; + String? telegram; + String? wechat; + String? linkedin; + String? discord; + String? whitepaper; + String? tokenPriceUSD; + String? image; + + Erc20TokenInfoExplorers({ + this.contractAddress, + this.tokenName, + this.symbol, + this.divisor, + this.tokenType, + this.totalSupply, + this.blueCheckmark, + this.description, + this.website, + this.email, + this.blog, + this.reddit, + this.slack, + this.facebook, + this.twitter, + this.bitcointalk, + this.github, + this.telegram, + this.wechat, + this.linkedin, + this.discord, + this.whitepaper, + this.tokenPriceUSD, + this.image, + }); + + Erc20TokenInfoExplorers.fromJson(Map json) { + contractAddress = json['contractAddress'] as String?; + tokenName = json['tokenName'] as String?; + symbol = json['symbol'] as String?; + divisor = json['divisor'] as String?; + tokenType = json['tokenType'] as String?; + totalSupply = json['totalSupply'] as String?; + blueCheckmark = json['blueCheckmark'] as String?; + description = json['description'] as String?; + website = json['website'] as String?; + email = json['email'] as String?; + blog = json['blog'] as String?; + reddit = json['reddit'] as String?; + slack = json['slack'] as String?; + facebook = json['facebook'] as String?; + twitter = json['twitter'] as String?; + bitcointalk = json['bitcointalk'] as String?; + github = json['github'] as String?; + telegram = json['telegram'] as String?; + wechat = json['wechat'] as String?; + linkedin = json['linkedin'] as String?; + discord = json['discord'] as String?; + whitepaper = json['whitepaper'] as String?; + tokenPriceUSD = json['tokenPriceUSD'] as String?; + image = json['image'] as String?; + } +} diff --git a/lib/entities/erc20_token_info_moralis.dart b/lib/entities/erc20_token_info_moralis.dart new file mode 100644 index 000000000..517ff9712 --- /dev/null +++ b/lib/entities/erc20_token_info_moralis.dart @@ -0,0 +1,85 @@ +class Erc20TokenInfoMoralis { + String? address; + String? addressLabel; + String? name; + String? symbol; + String? decimals; + String? logo; + String? logoHash; + String? thumbnail; + String? totalSupply; + String? totalSupplyFormatted; + String? fullyDilutedValuation; + String? blockNumber; + int? validated; + String? createdAt; + bool? possibleSpam; + bool? verifiedContract; + Links? links; + int? securityScore; + + Erc20TokenInfoMoralis({ + this.address, + this.addressLabel, + this.name, + this.symbol, + this.decimals, + this.logo, + this.logoHash, + this.thumbnail, + this.totalSupply, + this.totalSupplyFormatted, + this.fullyDilutedValuation, + this.blockNumber, + this.validated, + this.createdAt, + this.possibleSpam, + this.verifiedContract, + this.links, + this.securityScore, + }); + + Erc20TokenInfoMoralis.fromJson(Map json) { + address = json['address'] as String?; + addressLabel = json['address_label'] as String?; + name = json['name'] as String?; + symbol = json['symbol'] as String?; + decimals = json['decimals'] as String?; + logo = json['logo'] as String?; + logoHash = json['logo_hash'] as String?; + thumbnail = json['thumbnail'] as String?; + totalSupply = json['total_supply'] as String?; + totalSupplyFormatted = json['total_supply_formatted'] as String?; + fullyDilutedValuation = json['fully_diluted_valuation'] as String?; + blockNumber = json['block_number'] as String?; + validated = json['validated'] as int?; + createdAt = json['created_at'] as String?; + possibleSpam = json['possible_spam'] as bool?; + verifiedContract = json['verified_contract'] as bool; + links = + json['links'] != null ? new Links.fromJson(json['links'] as Map) : null; + securityScore = json['security_score'] as int?; + } +} + +class Links { + String? twitter; + String? website; + String? facebook; + String? reddit; + String? github; + String? linkedin; + String? telegram; + + Links({this.twitter, this.website, this.facebook, this.reddit}); + + Links.fromJson(Map json) { + twitter = json['twitter'] as String?; + website = json['website'] as String?; + facebook = json['facebook'] as String?; + reddit = json['reddit'] as String?; + github = json['github'] as String?; + linkedin = json['linkedin'] as String?; + telegram = json['telegram'] as String?; + } +} diff --git a/lib/src/screens/dashboard/edit_token_page.dart b/lib/src/screens/dashboard/edit_token_page.dart index dbb5f1aae..6f662e2be 100644 --- a/lib/src/screens/dashboard/edit_token_page.dart +++ b/lib/src/screens/dashboard/edit_token_page.dart @@ -2,16 +2,19 @@ import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/address_text_field.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/checkbox_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; class EditTokenPage extends BasePage { EditTokenPage({ @@ -79,7 +82,7 @@ class _EditTokenPageBodyState extends State { if (widget.token != null) { address = widget.homeSettingsViewModel.getTokenAddressBasedOnWallet(widget.token!); - + _contractAddressController.text = address ?? ''; _tokenNameController.text = widget.token!.name; _tokenSymbolController.text = widget.token!.title; @@ -135,8 +138,8 @@ class _EditTokenPageBodyState extends State { S.of(context).warning, style: TextStyle( fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).dialogTheme.backgroundColor, + fontWeight: FontWeight.bold, + color: Theme.of(context).extension()!.titleColor, ), ), Padding( @@ -145,7 +148,7 @@ class _EditTokenPageBodyState extends State { S.of(context).add_token_warning, style: TextStyle( fontSize: 14, - fontWeight: FontWeight.normal, + fontWeight: FontWeight.bold, color: Theme.of(context) .extension()! .detailsTitlesColor, @@ -167,6 +170,15 @@ class _EditTokenPageBodyState extends State { bottomSection: Column( children: [ if (_showDisclaimer) ...[ + Text( + S.current.do_not_send_funds_to_contract_address_warning, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + color: Theme.of(context).extension()!.titleColor, + ), + ), + SizedBox(height: 20), CheckboxWidget( value: _disclaimerChecked, caption: S.of(context).add_token_disclaimer_check, @@ -176,47 +188,84 @@ class _EditTokenPageBodyState extends State { ), SizedBox(height: 20), ], - Row( - children: [ - Expanded( - child: PrimaryButton( - onPressed: () async { - if (widget.token != null) { - await widget.homeSettingsViewModel.deleteToken(widget.token!); - } - Navigator.pop(context); - }, - text: widget.token != null ? S.of(context).delete : S.of(context).cancel, - color: Colors.red, - textColor: Colors.white, - ), - ), - SizedBox(width: 20), - Expanded( - child: PrimaryButton( - onPressed: () async { - if (_formKey.currentState!.validate() && - (!_showDisclaimer || _disclaimerChecked)) { - await widget.homeSettingsViewModel.addToken( - token: CryptoCurrency( - name: _tokenNameController.text, - title: _tokenSymbolController.text.toUpperCase(), - decimals: int.parse(_tokenDecimalController.text), - iconPath: _tokenIconPathController.text, - ), - contractAddress: _contractAddressController.text, - ); - if (mounted) { + Observer( + builder: (context) { + return Row( + children: [ + Expanded( + child: LoadingPrimaryButton( + isLoading: widget.homeSettingsViewModel.isDeletingToken, + onPressed: () async { + if (widget.token != null) { + await widget.homeSettingsViewModel.deleteToken(widget.token!); + } Navigator.pop(context); - } - } - }, - text: S.of(context).save, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - ), - ], + }, + text: widget.token != null ? S.of(context).delete : S.of(context).cancel, + color: Colors.red, + textColor: Colors.white, + ), + ), + SizedBox(width: 20), + Expanded( + child: LoadingPrimaryButton( + isLoading: widget.homeSettingsViewModel.isAddingToken || + widget.homeSettingsViewModel.isValidatingContractAddress, + onPressed: () async { + if (_formKey.currentState!.validate() && + (!_showDisclaimer || _disclaimerChecked)) { + final hasPotentialError = await widget.homeSettingsViewModel + .checkIfERC20TokenContractAddressIsAPotentialScamAddress( + _contractAddressController.text, + ); + final actionCall = () async { + await widget.homeSettingsViewModel.addToken( + token: CryptoCurrency( + name: _tokenNameController.text, + title: _tokenSymbolController.text.toUpperCase(), + decimals: int.parse(_tokenDecimalController.text), + iconPath: _tokenIconPathController.text, + ), + contractAddress: _contractAddressController.text, + ); + }; + + if (hasPotentialError) { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.current.warning, + alertContent: S.current.contract_warning, + rightButtonText: S.of(context).continue_text, + leftButtonText: S.of(context).cancel, + actionRightButton: () async { + Navigator.of(dialogContext).pop(); + await actionCall(); + if (mounted) { + Navigator.pop(context); + } + }, + actionLeftButton: () => Navigator.of(dialogContext).pop(), + ); + }, + ); + } else { + await actionCall(); + if (mounted) { + Navigator.pop(context); + } + } + } + }, + text: S.of(context).save, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + ], + ); + }, ), ], ), diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 5778f1e19..bb21f20ef 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -1,8 +1,14 @@ +import 'dart:convert'; +import 'dart:developer'; + import 'package:cake_wallet/core/fiat_conversion_service.dart'; +import 'package:cake_wallet/entities/erc20_token_info_explorers.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cake_wallet/entities/erc20_token_info_moralis.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; @@ -11,6 +17,8 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; +import 'package:http/http.dart' as http; +import 'package:cake_wallet/.secrets.g.dart' as secrets; part 'home_settings_view_model.g.dart'; @@ -18,7 +26,10 @@ class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewM abstract class HomeSettingsViewModelBase with Store { HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel) - : tokens = ObservableSet() { + : tokens = ObservableSet(), + isAddingToken = false, + isDeletingToken = false, + isValidatingContractAddress = false { _updateTokensList(); } @@ -27,6 +38,15 @@ abstract class HomeSettingsViewModelBase with Store { final ObservableSet tokens; + @observable + bool isAddingToken; + + @observable + bool isDeletingToken; + + @observable + bool isValidatingContractAddress; + @observable String searchText = ''; @@ -45,66 +65,263 @@ abstract class HomeSettingsViewModelBase with Store { @action void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value; + @action Future addToken({ required String contractAddress, required CryptoCurrency token, }) async { - if (_balanceViewModel.wallet.type == WalletType.ethereum) { - final erc20token = Erc20Token( - name: token.name, - symbol: token.title, - decimal: token.decimals, - contractAddress: contractAddress, - iconPath: token.iconPath, - ); + try { + isAddingToken = true; + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + final erc20token = Erc20Token( + name: token.name, + symbol: token.title, + decimal: token.decimals, + contractAddress: contractAddress, + iconPath: token.iconPath, + ); - await ethereum!.addErc20Token(_balanceViewModel.wallet, erc20token); + await ethereum!.addErc20Token(_balanceViewModel.wallet, erc20token); + } + + if (_balanceViewModel.wallet.type == WalletType.polygon) { + final polygonToken = Erc20Token( + name: token.name, + symbol: token.title, + decimal: token.decimals, + contractAddress: contractAddress, + iconPath: token.iconPath, + ); + await polygon!.addErc20Token(_balanceViewModel.wallet, polygonToken); + } + + if (_balanceViewModel.wallet.type == WalletType.solana) { + await solana!.addSPLToken( + _balanceViewModel.wallet, + token, + contractAddress, + ); + } + + if (_balanceViewModel.wallet.type == WalletType.tron) { + await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress); + } + + _updateTokensList(); + _updateFiatPrices(token); + } finally { + isAddingToken = false; } - - if (_balanceViewModel.wallet.type == WalletType.polygon) { - final polygonToken = Erc20Token( - name: token.name, - symbol: token.title, - decimal: token.decimals, - contractAddress: contractAddress, - iconPath: token.iconPath, - ); - await polygon!.addErc20Token(_balanceViewModel.wallet, polygonToken); - } - - if (_balanceViewModel.wallet.type == WalletType.solana) { - await solana!.addSPLToken( - _balanceViewModel.wallet, - token, - contractAddress, - ); - } - - if (_balanceViewModel.wallet.type == WalletType.tron) { - await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress); - } - - _updateTokensList(); - _updateFiatPrices(token); } + @action Future deleteToken(CryptoCurrency token) async { - if (_balanceViewModel.wallet.type == WalletType.ethereum) { - await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); - } + try { + isDeletingToken = true; + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); + } - if (_balanceViewModel.wallet.type == WalletType.polygon) { - await polygon!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); - } + if (_balanceViewModel.wallet.type == WalletType.polygon) { + await polygon!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); + } - if (_balanceViewModel.wallet.type == WalletType.solana) { - await solana!.deleteSPLToken(_balanceViewModel.wallet, token); - } + if (_balanceViewModel.wallet.type == WalletType.solana) { + await solana!.deleteSPLToken(_balanceViewModel.wallet, token); + } - if (_balanceViewModel.wallet.type == WalletType.tron) { - await tron!.deleteTronToken(_balanceViewModel.wallet, token); + if (_balanceViewModel.wallet.type == WalletType.tron) { + await tron!.deleteTronToken(_balanceViewModel.wallet, token); + } + _updateTokensList(); + } finally { + isDeletingToken = false; + } + } + + Future checkIfERC20TokenContractAddressIsAPotentialScamAddress( + String contractAddress, + ) async { + try { + isValidatingContractAddress = true; + + if (!isEVMCompatibleChain(_balanceViewModel.wallet.type)) { + return false; + } + + bool isEthereum = _balanceViewModel.wallet.type == WalletType.ethereum; + + bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis( + contractAddress, + isEthereum ? 'eth' : 'polygon', + ); + + bool isPotentialScamViaExplorers = await _isPotentialScamTokenViaExplorers( + contractAddress, + isEthereum: isEthereum, + ); + + bool isUnverifiedContract = await _isContractUnverified( + contractAddress, + isEthereum: isEthereum, + ); + + final showWarningForContractAddress = + isPotentialScamViaMoralis || isUnverifiedContract || isPotentialScamViaExplorers; + + return showWarningForContractAddress; + } finally { + isValidatingContractAddress = false; + } + } + + Future _isPotentialScamTokenViaMoralis( + String contractAddress, + String chainName, + ) async { + final uri = Uri.https( + 'deep-index.moralis.io', + '/api/v2.2/erc20/metadata', + { + "chain": chainName, + "addresses": contractAddress, + }, + ); + + try { + final response = await http.get( + uri, + headers: { + "Accept": "application/json", + "X-API-Key": secrets.moralisApiKey, + }, + ); + + final decodedResponse = jsonDecode(response.body); + + final tokenInfo = Erc20TokenInfoMoralis.fromJson(decodedResponse[0] as Map); + + // Based on analysis using Moralis internal metrics + if (tokenInfo.possibleSpam == true) { + return true; + } + + // Tokens whose contract have not been verified are potentially risky tokens. + if (tokenInfo.verifiedContract == false) { + return true; + } + + // Tokens with a security score less than 40 are potentially risky, requiring caution when dealing with them. + if (tokenInfo.securityScore == null || tokenInfo.securityScore! < 40) { + return true; + } + + // Absence of a website URL for an ERC-20 token can be a potential red flag. A legitimate ERC-20 projects should have a well-maintained website that provides information about the token, its purpose, team, and roadmap. + if (tokenInfo.links?.website == null || tokenInfo.links!.website!.isEmpty) { + return true; + } + + // Having a Fully Diluted Valiuation of 0 is a significant red flag that could signify: + // - An abandoned/unlaunched project + // - Incorrect/missing token data + // - Suspicious manipulation of token data + if (tokenInfo.fullyDilutedValuation == '0') { + return true; + } + + // I mean, a logo is the most basic of all the potential causes, but why does your fully functional project not have a logo? + if (tokenInfo.logo == null) { + return true; + } + + return false; + } catch (e) { + return true; + } + } + + Future _isPotentialScamTokenViaExplorers( + String contractAddress, { + required bool isEthereum, + }) async { + final uri = Uri.https( + isEthereum ? "api.etherscan.io" : "api.polygonscan.com", + "/api", + { + "module": "token", + "action": "tokeninfo", + "contractaddress": contractAddress, + "apikey": isEthereum ? secrets.etherScanApiKey : secrets.polygonScanApiKey, + }, + ); + + try { + final response = await http.get(uri); + + final decodedResponse = jsonDecode(response.body) as Map; + + if (decodedResponse['status'] != '1') { + log('${decodedResponse['result']}'); + return true; + } + + final tokenInfo = + Erc20TokenInfoExplorers.fromJson(decodedResponse['result'][0] as Map); + + // A token without an email to reach its creators is a potential red flag + if (tokenInfo.email?.isEmpty == true) { + return true; + } + + // A token without a website is a potential red flag + if (tokenInfo.website?.isEmpty == true) { + return true; + } + + // if (tokenInfo.whitepaper == null) { + // return true; + // } + + return false; + } catch (e) { + return true; + } + } + + Future _isContractUnverified( + String contractAddress, { + required bool isEthereum, + }) async { + final uri = Uri.https( + isEthereum ? "api.etherscan.io" : "api.polygonscan.com", + "/api", + { + "module": "contract", + "action": "getsourcecode", + "contractaddress": contractAddress, + "apikey": isEthereum ? secrets.etherScanApiKey : secrets.polygonScanApiKey, + }, + ); + + try { + final response = await http.get(uri); + + final decodedResponse = jsonDecode(response.body) as Map; + + if (decodedResponse['status'] == '0') { + log('${decodedResponse['result']}'); + return true; + } + + if (decodedResponse['status'] == '1' && + decodedResponse['result'][0]['ABI'] == 'Contract source code not verified') { + return true; // Contract is not verified + } else { + return false; // Contract is verified + } + } catch (e) { + return true; } - _updateTokensList(); } Future getToken(String contractAddress) async { diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 0b238ad23..cee29eeb4 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -160,6 +160,7 @@ "contact_name": "اسم جهة الاتصال", "contact_support": "اتصل بالدعم", "continue_text": "التالي", + "contract_warning": "تم وضع علامة على عنوان العقد هذا على أنه احتيالي محتمل. يرجى المعالجة بحذر.", "contractName": "ﺪﻘﻌﻟﺍ ﻢﺳﺍ", "contractSymbol": "ﺪﻘﻌﻟﺍ ﺰﻣﺭ", "copied_key_to_clipboard": "تم نسخ ${key} إلى الحافظة", @@ -219,6 +220,7 @@ "displayable": "قابل للعرض", "do_not_have_enough_gas_asset": "ليس لديك ما يكفي من ${currency} لإجراء معاملة وفقًا لشروط شبكة blockchain الحالية. أنت بحاجة إلى المزيد من ${currency} لدفع رسوم شبكة blockchain، حتى لو كنت ترسل أصلًا مختلفًا.", "do_not_send": "لا ترسل", + "do_not_send_funds_to_contract_address_warning": "لا ترسل أموالًا إلى هذا العنوان \n\n هذا مجرد معرف للرمز المميز ، فستضيع أي أموال تم إرسالها إلى هذا العنوان. \n\n ملاحظة: لن تطلب منك Cake إضافة عنوان عقد أبدًا", "do_not_share_warning_text": "لا تشارك هذه مع أي شخص آخر ، بما في ذلك الدعم.\n\nيمكن أن تتم سرقة أموالك!", "do_not_show_me": "لا ترني هذا مجددا", "domain_looks_up": "ﻝﺎﺠﻤﻟﺍ ﺚﺤﺑ ﺕﺎﻴﻠﻤﻋ", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index c13d6d7d6..771d58f57 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -160,6 +160,7 @@ "contact_name": "Име на контакт", "contact_support": "Свържи се с отдел поддръжка", "continue_text": "Напред", + "contract_warning": "Този адрес на договора е маркиран като потенциално измамник. Моля, обработете с повишено внимание.", "contractName": "Име на договора", "contractSymbol": "Договор Символ", "copied_key_to_clipboard": "Копиран ключ: ${key}", @@ -219,6 +220,7 @@ "displayable": "Възможност за показване", "do_not_have_enough_gas_asset": "Нямате достатъчно ${currency}, за да извършите транзакция с текущите условия на блокчейн мрежата. Имате нужда от повече ${currency}, за да платите таксите за блокчейн мрежа, дори ако изпращате различен актив.", "do_not_send": "Не изпращай", + "do_not_send_funds_to_contract_address_warning": "Не изпращайте средства на този адрес \n\n Това е само идентификатор за токена, всички средства, изпратени на този адрес", "do_not_share_warning_text": "Не споделяйте това с никого, дори и отдел поддръжка.\n\nПарите Ви могат и ще бъдат откраднати!", "do_not_show_me": "Не показвай повече това", "domain_looks_up": "Търсене на домейни", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 49bc856fd..33039c741 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -160,6 +160,7 @@ "contact_name": "Jméno kontaktu", "contact_support": "Kontaktovat podporu", "continue_text": "Pokračovat", + "contract_warning": "Tato adresa smlouvy byla označena jako potenciálně podvodná. Zpracovejte prosím opatrně.", "contractName": "Název smlouvy", "contractSymbol": "Symbol smlouvy", "copied_key_to_clipboard": "Zkopírován ${key} do schránky", @@ -219,6 +220,7 @@ "displayable": "Zobrazitelné", "do_not_have_enough_gas_asset": "Nemáte dostatek ${currency} k provedení transakce s aktuálními podmínkami blockchainové sítě. K placení poplatků za blockchainovou síť potřebujete více ${currency}, i když posíláte jiné aktivum.", "do_not_send": "Neodesílat", + "do_not_send_funds_to_contract_address_warning": "Neposílejte finanční prostředky na tuto adresu \n\n Toto je pouze identifikátor pro token, jakékoli prostředky zaslané na tuto adresu budou ztraceny.", "do_not_share_warning_text": "Toto nesdílejte s nikým jiným, ani s podporou.\n\nJinak mohou být Vaše prostředky ukradeny!", "do_not_show_me": "Příště nezobrazovat", "domain_looks_up": "Vyhledávání domén", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 19b7fbc90..98429ea04 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -160,6 +160,7 @@ "contact_name": "Name des Kontakts", "contact_support": "Support kontaktieren", "continue_text": "Weiter", + "contract_warning": "Diese Vertragsadresse wurde als potenziell betrügerisch gekennzeichnet. Bitte verarbeiten Sie mit Vorsicht.", "contractName": "Vertragsname", "contractSymbol": "Vertragssymbol", "copied_key_to_clipboard": "${key} in Zwischenablage kopiert", @@ -219,6 +220,7 @@ "displayable": "Anzeigebar", "do_not_have_enough_gas_asset": "Sie verfügen nicht über genügend ${currency}, um eine Transaktion unter den aktuellen Bedingungen des Blockchain-Netzwerks durchzuführen. Sie benötigen mehr ${currency}, um die Gebühren für das Blockchain-Netzwerk zu bezahlen, auch wenn Sie einen anderen Vermögenswert senden.", "do_not_send": "Nicht senden", + "do_not_send_funds_to_contract_address_warning": "Senden Sie keine Mittel an diese Adresse \n\n Dies ist nur eine Kennung für das Token. Alle an diese Adresse gesendeten Mittel gehen verloren. \n\n Hinweis: Kuchen würde Sie niemals auffordern, eine Vertragsadresse hinzuzufügen", "do_not_share_warning_text": "Teilen Sie diese nicht mit anderen, einschließlich Support.\n\nIhr Geld kann und wird gestohlen werden!", "do_not_show_me": "Zeig mir das nicht noch einmal", "domain_looks_up": "Domain-Suchen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 75c0fc9c0..7512c3ca0 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -160,6 +160,7 @@ "contact_name": "Contact Name", "contact_support": "Contact Support", "continue_text": "Continue", + "contract_warning": "This contract address has been flagged as potentially fraudulent. Please process with caution.", "contractName": "Contract Name", "contractSymbol": "Contract Symbol", "copied_key_to_clipboard": "Copied ${key} to Clipboard", @@ -219,6 +220,7 @@ "displayable": "Displayable", "do_not_have_enough_gas_asset": "You do not have enough ${currency} to make a transaction with the current blockchain network conditions. You need more ${currency} to pay blockchain network fees, even if you are sending a different asset.", "do_not_send": "Don't send", + "do_not_send_funds_to_contract_address_warning": "Do not send funds to this address\n\n This is just an identifier for the token, any funds sent to this address will be lost.\n\n NOTE: Cake would never ask you to add a contract address", "do_not_share_warning_text": "Do not share these with anyone else, including support.\n\nYour funds can and will be stolen!", "do_not_show_me": "Do not show me this again", "domain_looks_up": "Domain lookups", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 73a2e77b9..66e17f1d5 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -160,6 +160,7 @@ "contact_name": "Nombre de contacto", "contact_support": "Contactar con Soporte", "continue_text": "Continuar", + "contract_warning": "Esta dirección de contrato ha sido marcada como potencialmente fraudulenta. Por favor, procese con precaución.", "contractName": "Nombre del contrato", "contractSymbol": "Símbolo de contrato", "copied_key_to_clipboard": "Copiado ${key} al portapapeles", @@ -219,6 +220,7 @@ "displayable": "Visualizable", "do_not_have_enough_gas_asset": "No tienes suficiente ${currency} para realizar una transacción con las condiciones actuales de la red blockchain. Necesita más ${currency} para pagar las tarifas de la red blockchain, incluso si envía un activo diferente.", "do_not_send": "no enviar", + "do_not_send_funds_to_contract_address_warning": "No envíe fondos a esta dirección \n\n Esto es solo un identificador para el token, se perderán los fondos enviados a esta dirección. \n\n Nota: Cake nunca le pediría que agregue una dirección de contrato", "do_not_share_warning_text": "No comparta estos con nadie más, incluido el soporte.\n\n¡Sus fondos pueden ser y serán robados!", "do_not_show_me": "no me muestres esto otra vez", "domain_looks_up": "Búsquedas de dominio", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index e1f557060..da73d0c00 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -160,6 +160,7 @@ "contact_name": "Nom de Contact", "contact_support": "Contacter l'assistance", "continue_text": "Continuer", + "contract_warning": "Cette adresse contractuelle a été signalée comme potentiellement frauduleuse. Veuillez traiter avec prudence.", "contractName": "Nom du contrat", "contractSymbol": "Symbole du contrat", "copied_key_to_clipboard": "${key} copiée vers le presse-papier", @@ -219,6 +220,7 @@ "displayable": "Visible", "do_not_have_enough_gas_asset": "Vous n'avez pas assez de ${currency} pour effectuer une transaction avec les conditions actuelles du réseau blockchain. Vous avez besoin de plus de ${currency} pour payer les frais du réseau blockchain, même si vous envoyez un actif différent.", "do_not_send": "Ne pas envoyer", + "do_not_send_funds_to_contract_address_warning": "N'envoyez pas de fonds à cette adresse \n\n Ceci est juste un identifiant pour le jeton, tous les fonds envoyés à cette adresse seront perdus. \n\n Remarque: Le gâteau ne vous demanderait jamais d'ajouter une adresse de contrat", "do_not_share_warning_text": "Ne les partagez avec personne, y compris avec l'assistance.\n\nVos fonds seraient inmanquablement volés !", "do_not_show_me": "Ne plus me montrer ceci à l'avenir", "domain_looks_up": "Résolution de nom", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 4e0eb83f9..387c1a0b0 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -160,6 +160,7 @@ "contact_name": "Sunan Tuntuɓi", "contact_support": "Tuntuɓi Support", "continue_text": "Ci gaba", + "contract_warning": "An kafa wannan adireshin kwantaragin kwangilar yayin da yuwuwar zamba. Da fatan za a aiwatar da taka tsantsan.", "contractName": "Sunan Kwangila", "contractSymbol": "Alamar Kwangila", "copied_key_to_clipboard": "An kwafa ${key} a cikin kwafin", @@ -219,6 +220,7 @@ "displayable": "Ana iya nunawa", "do_not_have_enough_gas_asset": "Ba ku da isassun ${currency} don yin ma'amala tare da yanayin cibiyar sadarwar blockchain na yanzu. Kuna buƙatar ƙarin ${currency} don biyan kuɗaɗen cibiyar sadarwar blockchain, koda kuwa kuna aika wata kadara daban.", "do_not_send": "Kada ka aika", + "do_not_send_funds_to_contract_address_warning": "Kada ku aika da kudade zuwa wannan adireshin \n\n Wannan kawai mai ganowa ne kawai don token, kowane asusu da aka aiko zuwa wannan adireshin za a rasa. # Lafazin ba zai taba tambayar ka ƙara adireshin kwangila", "do_not_share_warning_text": "Kada ku raba waɗannan ga kowa, gami da tallafi.\n\nZa a iya sace kuɗin ku kuma za a sace!", "do_not_show_me": "Kar ka sake nuna min wannan", "domain_looks_up": "Binciken yanki", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index f77547992..a0d826c98 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -160,6 +160,7 @@ "contact_name": "संपर्क नाम", "contact_support": "सहायता से संपर्क करें", "continue_text": "जारी रहना", + "contract_warning": "इस अनुबंध के पते को संभावित रूप से धोखाधड़ी के रूप में चिह्नित किया गया है। कृपया सावधानी के साथ प्रक्रिया करें।", "contractName": "अनुबंध का नाम", "contractSymbol": "अनुबंध चिह्न", "copied_key_to_clipboard": "की नकल की ${key} क्लिपबोर्ड पर", @@ -219,6 +220,7 @@ "displayable": "प्रदर्शन योग्य", "do_not_have_enough_gas_asset": "वर्तमान ब्लॉकचेन नेटवर्क स्थितियों में लेनदेन करने के लिए आपके पास पर्याप्त ${currency} नहीं है। ब्लॉकचेन नेटवर्क शुल्क का भुगतान करने के लिए आपको अधिक ${currency} की आवश्यकता है, भले ही आप एक अलग संपत्ति भेज रहे हों।", "do_not_send": "मत भेजो", + "do_not_send_funds_to_contract_address_warning": "इस पते पर धन न भेजें \n\n यह सिर्फ टोकन के लिए एक पहचानकर्ता है, इस पते पर भेजे गए किसी भी धन को खो दिया जाएगा। \n\n नोट: केक आपको एक अनुबंध पता जोड़ने के लिए कभी नहीं कहेगा", "do_not_share_warning_text": "समर्थन सहित, इन्हें किसी और के साथ साझा न करें।\n\nआपके धन की चोरी हो सकती है और होगी!", "do_not_show_me": "मुझे यह फिर न दिखाएं", "domain_looks_up": "डोमेन लुकअप", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index db02c5552..6a7214201 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -160,6 +160,7 @@ "contact_name": "Ime kontakta", "contact_support": "Kontaktirajte podršku", "continue_text": "Nastavak", + "contract_warning": "Ova adresa ugovora označena je kao potencijalno lažna. Molimo obradite s oprezom.", "contractName": "Naziv ugovora", "contractSymbol": "Simbol ugovora", "copied_key_to_clipboard": "${key} kopiran u međuspremnik", @@ -219,6 +220,7 @@ "displayable": "Dostupno za prikaz", "do_not_have_enough_gas_asset": "Nemate dovoljno ${currency} da izvršite transakciju s trenutačnim uvjetima blockchain mreže. Trebate više ${currency} da platite naknade za blockchain mrežu, čak i ako šaljete drugu imovinu.", "do_not_send": "Ne šalji", + "do_not_send_funds_to_contract_address_warning": "Ne šaljite sredstva na ovu adresu \n\n Ovo je samo identifikator za token, izgubit će se bilo koja sredstva poslana na ovu adresu. \n\n Napomena: Torta nikad ne bi tražila da dodate adresu ugovora", "do_not_share_warning_text": "Nemojte ih dijeliti ni s kim, uključujući podršku.\n\nVaša sredstva mogu i bit će ukradena!", "do_not_show_me": "Ne pokazuj mi ovo više", "domain_looks_up": "Pretraga domena", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 178254bba..21b940a63 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -160,6 +160,7 @@ "contact_name": "Կոնտակտի անուն", "contact_support": "Հետադարձ կապ", "continue_text": "Շարունակել", + "contract_warning": "Պայմանագրի այս հասցեն դրոշմել է որպես հնարավոր կեղծ: Խնդրում ենք զգուշությամբ մշակել:", "contractName": "Գործարքի անուն", "contractSymbol": "Գործարքի Նշան", "copied_key_to_clipboard": "${key} պատճենված է տեքստի բուֆերում", @@ -218,6 +219,7 @@ "displayable": "Ցուցադրվող", "do_not_have_enough_gas_asset": "Դուք չունեք բավարար ${currency} տրանզակցիան կատարելու համար ընթացիկ բլոկչեյն ցանցի պայմաններում: Դուք պետք է ունենաք ավելի շատ ${currency} blockchain ցանցի միջնորդավճարները վճարելու համար, նույնիսկ եթե դուք այլ ակտիվ եք ուղարկում:", "do_not_send": "Մի ուղարկեք", + "do_not_send_funds_to_contract_address_warning": "Այս հասցեին գումարներ մի ուղարկեք \n\n Սա պարզապես նույնականացն է նշանի համար, այս հասցեով ուղարկված ցանկացած միջոց կկորչի: \n\n Նշում. Տորթը երբեք չի խնդրի ձեզ ավելացնել պայմանագրի հասցե", "do_not_share_warning_text": "Մի կիսեք այս տեղեկատվությունը որևէ մեկի հետ, այդ թվում նաև աջակցության հետ: \n\nՁեր միջոցները կարող են գողանալ կորցնել!", "do_not_show_me": "Մի ցուցադրեք ինձ նորից", "domain_looks_up": "Դոմեյնի որոնում", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index e67402de2..f677a65a6 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -160,6 +160,7 @@ "contact_name": "Nama Kontak", "contact_support": "Hubungi Dukungan", "continue_text": "Lanjutkan", + "contract_warning": "Alamat kontrak ini telah ditandai sebagai berpotensi curang. Silakan memproses dengan hati -hati.", "contractName": "Nama Kontrak", "contractSymbol": "Simbol Kontrak", "copied_key_to_clipboard": "Kunci ${key} disalin ke Clipboard", @@ -219,6 +220,7 @@ "displayable": "Dapat ditampilkan", "do_not_have_enough_gas_asset": "Anda tidak memiliki cukup ${currency} untuk melakukan transaksi dengan kondisi jaringan blockchain saat ini. Anda memerlukan lebih banyak ${currency} untuk membayar biaya jaringan blockchain, meskipun Anda mengirimkan aset yang berbeda.", "do_not_send": "Jangan kirim", + "do_not_send_funds_to_contract_address_warning": "Jangan mengirim dana ke alamat ini \n\n Ini hanya pengidentifikasi untuk token, dana apa pun yang dikirim ke alamat ini akan hilang. \n\n Catatan: Kue tidak akan pernah meminta Anda untuk menambahkan alamat kontrak", "do_not_share_warning_text": "Jangan berikan ini pada siapapun, termasuk dukungan.\n\nDana Anda bisa dan akan dicuri!", "do_not_show_me": "Jangan tampilkan ini lagi", "domain_looks_up": "Pencarian domain", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a764520d8..94cd0fa1d 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -161,6 +161,7 @@ "contact_name": "Nome Contatto", "contact_support": "Contatta l'assistenza", "continue_text": "Continua", + "contract_warning": "Questo indirizzo del contratto è stato contrassegnato come potenzialmente fraudolento. Si prega di elaborare con cautela.", "contractName": "Nome del contratto", "contractSymbol": "Simbolo del contratto", "copied_key_to_clipboard": " ${key} copiata negli Appunti", @@ -220,6 +221,7 @@ "displayable": "Visualizzabile", "do_not_have_enough_gas_asset": "Non hai abbastanza ${currency} per effettuare una transazione con le attuali condizioni della rete blockchain. Hai bisogno di più ${currency} per pagare le commissioni della rete blockchain, anche se stai inviando una risorsa diversa.", "do_not_send": "Non inviare", + "do_not_send_funds_to_contract_address_warning": "Non inviare fondi a questo indirizzo \n\n Questo è solo un identificatore per il token, qualsiasi fondi inviati a questo indirizzo andrà perso. \n\n Nota: la torta non ti chiederebbe mai di aggiungere un indirizzo contrattuale", "do_not_share_warning_text": "Non condividerli con nessun altro, incluso il supporto.\n\nI tuoi fondi possono e saranno rubati!", "do_not_show_me": "Non mostrarmelo di nuovo", "domain_looks_up": "Ricerche di domini", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index bda62536e..2dd24346c 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -160,6 +160,7 @@ "contact_name": "連絡先", "contact_support": "サポートに連絡する", "continue_text": "持続する", + "contract_warning": "この契約住所は、潜在的に不正としてフラグが立てられています。注意して処理してください。", "contractName": "契約名", "contractSymbol": "契約記号", "copied_key_to_clipboard": "コピー済み ${key} クリップボードへ", @@ -219,6 +220,7 @@ "displayable": "表示可能", "do_not_have_enough_gas_asset": "現在のブロックチェーン ネットワークの状況では、トランザクションを行うのに十分な ${currency} がありません。別のアセットを送信する場合でも、ブロックチェーン ネットワーク料金を支払うにはさらに ${currency} が必要です。", "do_not_send": "送信しない", + "do_not_send_funds_to_contract_address_warning": "この住所に資金を送らないでください\n\nこれはトークンの識別子であり、この住所に送られた資金は失われます。", "do_not_share_warning_text": "サポートを含め、これらを他の誰とも共有しないでください。\n\nあなたの資金は盗まれる可能性があります!", "do_not_show_me": "また僕にこれを見せないでください", "domain_looks_up": "ドメイン検索", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index df339df55..9780de120 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -160,6 +160,7 @@ "contact_name": "담당자 이름", "contact_support": "지원팀에 문의", "continue_text": "잇다", + "contract_warning": "이 계약 주소는 잠재적으로 사기성으로 표시되었습니다. 주의해서 처리하십시오.", "contractName": "계약명", "contractSymbol": "계약 기호", "copied_key_to_clipboard": "복사 ${key} 클립 보드로", @@ -219,6 +220,7 @@ "displayable": "표시 가능", "do_not_have_enough_gas_asset": "현재 블록체인 네트워크 조건으로 거래를 하기에는 ${currency}이(가) 충분하지 않습니다. 다른 자산을 보내더라도 블록체인 네트워크 수수료를 지불하려면 ${currency}가 더 필요합니다.", "do_not_send": "보내지 마세요", + "do_not_send_funds_to_contract_address_warning": "이 주소로 자금을 보내지 마십시오 \n\n 이것은 토큰의 식별자 일뿐입니다.이 주소로 전송 된 모든 자금은 손실됩니다. \n\n 참고 : Cake는 계약서 주소를 추가하도록 요구하지 않습니다.", "do_not_share_warning_text": "지원을 포함하여 다른 사람과 이러한 정보를 공유하지 마십시오.\n\n귀하의 자금은 도난당할 수 있고 도난당할 수 있습니다!", "do_not_show_me": "나를 다시 표시하지 않음", "domain_looks_up": "도메인 조회", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index e988c6c8d..af28f5e9b 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -160,6 +160,7 @@ "contact_name": "ဆက်သွယ်ရန်အမည်", "contact_support": "ပံ့ပိုးကူညီမှုထံ ဆက်သွယ်ပါ။", "continue_text": "ဆက်လက်", + "contract_warning": "ဒီစာချုပ်လိပ်စာအလားအလာအလားအလာအလားအလာအလံများကိုအလံလွှင့်တင်ခဲ့သည်။ ကျေးဇူးပြုပြီးသတိဖြင့်လုပ်ငန်းစဉ်။", "contractName": "စာချုပ်အမည်", "contractSymbol": "စာချုပ်သင်္ကေတ", "copied_key_to_clipboard": "${key} ကို Clipboard သို့ ကူးယူထားသည်။", @@ -219,6 +220,7 @@ "displayable": "ပြသနိုင်သည်။", "do_not_have_enough_gas_asset": "လက်ရှိ blockchain ကွန်ရက်အခြေအနေများနှင့် အရောင်းအဝယ်ပြုလုပ်ရန် သင့်တွင် ${currency} လုံလောက်မှုမရှိပါ။ သင်သည် မတူညီသော ပိုင်ဆိုင်မှုတစ်ခုကို ပေးပို့နေသော်လည်း blockchain ကွန်ရက်အခကြေးငွေကို ပေးဆောင်ရန် သင်သည် နောက်ထပ် ${currency} လိုအပ်ပါသည်။", "do_not_send": "မပို့ပါနှင့်", + "do_not_send_funds_to_contract_address_warning": "ဒီလိပ်စာကိုရန်ပုံငွေမပို့ပါနဲ့။ ဒီလိပ်စာကိုအမှတ်အသားမပို့ပါနဲ့, ဒီလိပ်စာကိုပို့တဲ့ရန်ပုံငွေမဆိုဆုံးရှုံးသွားမှာပါ။ \n\n မှတ်ချက်။ ။ \n\n မှတ်ချက်။ ။", "do_not_share_warning_text": "ပံ့ပိုးကူညီမှုအပါအဝင် ဤအရာများကို အခြားမည်သူနှင့်မျှ မမျှဝေပါနှင့်။\n\nသင့်ငွေများကို ခိုးယူခံရနိုင်သည်!", "do_not_show_me": "ဒါကို ထပ်မပြနဲ့", "domain_looks_up": "ဒိုမိန်းရှာဖွေမှုများ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index d1f6032e5..c6a107d6d 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -160,6 +160,7 @@ "contact_name": "Contactnaam", "contact_support": "Contact opnemen met ondersteuning", "continue_text": "Doorgaan met", + "contract_warning": "Dit contractadres is gemarkeerd als mogelijk frauduleus. Verwerk met voorzichtigheid.", "contractName": "Contractnaam", "contractSymbol": "Contractsymbool", "copied_key_to_clipboard": "Gekopieerd ${key} naar het klembord", @@ -219,6 +220,7 @@ "displayable": "Weer te geven", "do_not_have_enough_gas_asset": "U heeft niet genoeg ${currency} om een transactie uit te voeren met de huidige blockchain-netwerkomstandigheden. U heeft meer ${currency} nodig om blockchain-netwerkkosten te betalen, zelfs als u een ander item verzendt.", "do_not_send": "Niet sturen", + "do_not_send_funds_to_contract_address_warning": "Stuur geen fondsen naar dit adres \n\n Dit is slechts een identificatie voor het token, alle fondsen die naar dit adres zijn verzonden, gaan verloren. \n\n Opmerking: cake zou u nooit vragen om een ​​contractadres toe te voegen", "do_not_share_warning_text": "Deel deze met niemand anders, ook niet met support.\n\nUw geld kan en zal worden gestolen!", "do_not_show_me": "laat me dit niet opnieuw zien", "domain_looks_up": "Domein opzoeken", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 3b4e0f81f..85e75f40c 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -160,6 +160,7 @@ "contact_name": "Nazwa Kontaktu", "contact_support": "Skontaktuj się z pomocą techniczną", "continue_text": "Dalej", + "contract_warning": "Ten adres umowy został oznaczony jako potencjalnie nieuczciwy. Prosimy o ostrożność.", "contractName": "Nazwa umowy", "contractSymbol": "Symbol kontraktu", "copied_key_to_clipboard": "Skopiowaneo ${key} do schowka", @@ -219,6 +220,7 @@ "displayable": "Wyświetlane", "do_not_have_enough_gas_asset": "Nie masz wystarczającej ilości ${currency}, aby dokonać transakcji przy bieżących warunkach sieci blockchain. Potrzebujesz więcej ${currency}, aby uiścić opłaty za sieć blockchain, nawet jeśli wysyłasz inny zasób.", "do_not_send": "Nie wysyłaj", + "do_not_send_funds_to_contract_address_warning": "Nie wysyłaj środków na ten adres \n\n To jest tylko identyfikator tokena, wszelkie środki wysłane na ten adres zostaną utracone. \n\n", "do_not_share_warning_text": "NIE udostępniaj ich nikomu innemu, w tym pomocy technicznej.\n\nTwoje środki wtedy prawdopodobnie zostaną skradzione!", "do_not_show_me": "Nie pokazuj mi tego ponownie", "domain_looks_up": "Wyszukiwanie domen", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 6b9e4bf77..9283ca691 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -160,6 +160,7 @@ "contact_name": "Nome do contato", "contact_support": "Contatar Suporte", "continue_text": "Continuar", + "contract_warning": "Este endereço do contrato foi sinalizado como potencialmente fraudulento. Por favor, processe com cautela.", "contractName": "Nome do contrato", "contractSymbol": "Símbolo do Contrato", "copied_key_to_clipboard": "${key} copiada para a área de transferência", @@ -219,6 +220,7 @@ "displayable": "Exibível", "do_not_have_enough_gas_asset": "Você não tem ${currency} suficiente para fazer uma transação com as condições atuais da rede blockchain. Você precisa de mais ${currency} para pagar as taxas da rede blockchain, mesmo se estiver enviando um ativo diferente.", "do_not_send": "não envie", + "do_not_send_funds_to_contract_address_warning": "Não envie fundos para este endereço \n\n Este é apenas um identificador para o token, quaisquer fundos enviados para este endereço serão perdidos. \n\n NOTA: O bolo nunca solicitaria que você adicione um endereço de contrato", "do_not_share_warning_text": "Não os compartilhe com mais ninguém, incluindo suporte.\n\nSeus fundos podem e serão roubados!", "do_not_show_me": "não me mostre isso novamente", "domain_looks_up": "Pesquisas de domínio", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index a8def9179..bbdc9f745 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -160,6 +160,7 @@ "contact_name": "Имя контакта", "contact_support": "Связаться со службой поддержки", "continue_text": "Продолжить", + "contract_warning": "Этот адрес контракта был отмечен как потенциально мошеннический. Пожалуйста, обработайтесь с осторожностью.", "contractName": "Название контракта", "contractSymbol": "Символ контракта", "copied_key_to_clipboard": "Скопировано ${key} в буфер обмена", @@ -219,6 +220,7 @@ "displayable": "Отображаемый", "do_not_have_enough_gas_asset": "У вас недостаточно ${currency} для совершения транзакции при текущих условиях сети блокчейн. Вам нужно больше ${currency} для оплаты комиссий за сеть блокчейна, даже если вы отправляете другой актив.", "do_not_send": "Не отправлять", + "do_not_send_funds_to_contract_address_warning": "Не отправляйте средства на этот адрес \n\n Это просто идентификатор для токена, любые средства, отправленные на этот адрес, будут потеряны. \n\n Примечание: торт никогда не попросит вас добавить адрес контракта", "do_not_share_warning_text": "Не сообщайте их никому, включая техподдержку.\n\nВаши средства могут и будут украдены!", "do_not_show_me": "Не показывай мне это больше", "domain_looks_up": "Поиск доменов", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 02480d6fd..3161f60d7 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -160,6 +160,7 @@ "contact_name": "ชื่อผู้ติดต่อ", "contact_support": "ติดต่อฝ่ายสนับสนุน", "continue_text": "ดำเนินการต่อ", + "contract_warning": "ที่อยู่สัญญานี้ได้รับการตั้งค่าสถานะว่าเป็นการฉ้อโกง กรุณาดำเนินการด้วยความระมัดระวัง", "contractName": "ชื่อสัญญา", "contractSymbol": "สัญลักษณ์สัญญา", "copied_key_to_clipboard": "คัดลอก ${key} ไปยัง Clipboard แล้ว", @@ -219,6 +220,7 @@ "displayable": "สามารถแสดงได้", "do_not_have_enough_gas_asset": "คุณมี ${currency} ไม่เพียงพอที่จะทำธุรกรรมกับเงื่อนไขเครือข่ายบล็อคเชนในปัจจุบัน คุณต้องมี ${currency} เพิ่มขึ้นเพื่อชำระค่าธรรมเนียมเครือข่ายบล็อคเชน แม้ว่าคุณจะส่งสินทรัพย์อื่นก็ตาม", "do_not_send": "อย่าส่ง", + "do_not_send_funds_to_contract_address_warning": "อย่าส่งเงินไปยังที่อยู่นี้ \n\n นี่เป็นเพียงตัวระบุสำหรับโทเค็นเงินทุนใด ๆ ที่ส่งไปยังที่อยู่นี้จะหายไป \n\n หมายเหตุ: เค้กจะไม่ขอให้คุณเพิ่มที่อยู่สัญญา", "do_not_share_warning_text": "อย่าแชร์ข้อมูลนี้กับใครอื่น รวมถึงฝ่ายสนับสนุนด้วย\n\nการเงินของคุณอาจถูกขโมยโดยไม่หวังดี!", "do_not_show_me": "อย่าแสดงข้อความนี้อีก", "domain_looks_up": "การค้นหาโดเมน", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 2307cc62b..54c8c4c01 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -160,6 +160,7 @@ "contact_name": "Pangalan ng Contact", "contact_support": "Makipag-ugnay sa Suporta", "continue_text": "Magpatuloy", + "contract_warning": "Ang address ng kontrata na ito ay na -flag bilang potensyal na mapanlinlang. Mangyaring iproseso nang may pag -iingat.", "contractName": "Pangalan ng Kontrata", "contractSymbol": "Simbolo ng Kontrata", "copied_key_to_clipboard": "Kinopya ang ${key} sa Clipboard", @@ -219,6 +220,7 @@ "displayable": "Maipapakita", "do_not_have_enough_gas_asset": "Wala kang sapat na ${currency} para gumawa ng transaksyon sa kasalukuyang kundisyon ng network ng blockchain. Kailangan mo ng higit pang ${currency} upang magbayad ng mga fee sa network ng blockchain, kahit na nagpapadala ka ng ibang asset.", "do_not_send": "Huwag ipadala", + "do_not_send_funds_to_contract_address_warning": "Huwag magpadala ng pondo sa address na ito\n\n Ito ay isang identifier lamang para sa token, ang anumang pondo na ipinadala sa address na ito ay mawawala.\n\n Tandaan: Ang cake ay hindi hihilingin sa iyo na magdagdag ng isang address ng kontrata", "do_not_share_warning_text": "Huwag ibahagi ang mga ito sa sinuman kasama ang tagatustos.\n\nMaaaring manakaw ang iyong mga pondo!", "do_not_show_me": "Huwag mo itong ipakita muli", "domain_looks_up": "Mga paghahanap ng domain", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 8346703bc..dc0040002 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -160,6 +160,7 @@ "contact_name": "Kişi ismi", "contact_support": "Destek ile İletişime Geç", "continue_text": "Devam et", + "contract_warning": "Bu sözleşme adresi potansiyel olarak hileli olarak işaretlenmiştir. Lütfen dikkatle işleyin.", "contractName": "Sözleşme Adı", "contractSymbol": "Sözleşme Sembolü", "copied_key_to_clipboard": "${key} panoya kopyalandı", @@ -219,6 +220,7 @@ "displayable": "Gösterilebilir", "do_not_have_enough_gas_asset": "Mevcut blockchain ağ koşullarıyla işlem yapmak için yeterli ${currency} paranız yok. Farklı bir varlık gönderiyor olsanız bile blockchain ağ ücretlerini ödemek için daha fazla ${currency} miktarına ihtiyacınız var.", "do_not_send": "Gönderme", + "do_not_send_funds_to_contract_address_warning": "Bu adrese fon göndermeyin \n\n Bu sadece jeton için bir tanımlayıcıdır, bu adrese gönderilen fonlar kaybolacaktır. \n\n Not: Kekten asla bir sözleşme adresi eklemenizi istemez", "do_not_share_warning_text": "Bunları destek de dahil olmak üzere başka kimseyle paylaşma.\n\nParan çalınabilir ve çalınacaktır!", "do_not_show_me": "Bana bunu bir daha gösterme", "domain_looks_up": "Etki alanı aramaları", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ec2324544..a53d54492 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -160,6 +160,7 @@ "contact_name": "Ім'я контакту", "contact_support": "Звернутися до служби підтримки", "continue_text": "Продовжити", + "contract_warning": "Ця адреса контракту була позначена як потенційно шахрайська. Будь ласка, обробляйте обережно.", "contractName": "Назва контракту", "contractSymbol": "Контракт символ", "copied_key_to_clipboard": "Скопійовано ${key} в буфер обміну", @@ -219,6 +220,7 @@ "displayable": "Відображуваний", "do_not_have_enough_gas_asset": "У вас недостатньо ${currency}, щоб здійснити трансакцію з поточними умовами мережі блокчейн. Вам потрібно більше ${currency}, щоб сплатити комісію мережі блокчейн, навіть якщо ви надсилаєте інший актив.", "do_not_send": "Не надсилайте", + "do_not_send_funds_to_contract_address_warning": "Не надсилайте кошти на цю адресу \n\n Це лише ідентифікатор для маркера, будь -які кошти, надіслані на цю адресу", "do_not_share_warning_text": "Не діліться цим нікому, включно зі службою підтримки.\n\nВаші кошти можуть і будуть вкрадені!", "do_not_show_me": "Не показуй мені це знову", "domain_looks_up": "Пошук доменів", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index bc5fc332c..65c2a768c 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -160,6 +160,7 @@ "contact_name": "رابطے کا نام", "contact_support": "سپورٹ سے رابطہ کریں۔", "continue_text": "جاری رہے", + "contract_warning": "اس معاہدے کے پتے کو ممکنہ طور پر جعلی قرار دیا گیا ہے۔ براہ کرم احتیاط کے ساتھ کارروائی کریں۔", "contractName": "ﻡﺎﻧ ﺎﮐ ﮦﺪﮨﺎﻌﻣ", "contractSymbol": "ﺖﻣﻼﻋ ﯽﮐ ﮦﺪﮨﺎﻌﻣ", "copied_key_to_clipboard": "${key} کو کلپ بورڈ پر کاپی کیا گیا۔", @@ -219,6 +220,7 @@ "displayable": "قابل نمائش", "do_not_have_enough_gas_asset": "آپ کے پاس موجودہ بلاکچین نیٹ ورک کی شرائط کے ساتھ لین دین کرنے کے لیے کافی ${currency} نہیں ہے۔ آپ کو بلاکچین نیٹ ورک کی فیس ادا کرنے کے لیے مزید ${currency} کی ضرورت ہے، چاہے آپ کوئی مختلف اثاثہ بھیج رہے ہوں۔", "do_not_send": "مت بھیجیں۔", + "do_not_send_funds_to_contract_address_warning": "اس پتے پر فنڈز نہ بھیجیں \n\n یہ ٹوکن کے لئے صرف ایک شناخت کنندہ ہے ، اس پتے پر بھیجے گئے کوئی بھی فنڈز ضائع ہوجائیں گے۔ \n\n نوٹ: کیک آپ کو کبھی بھی معاہدہ کا پتہ شامل کرنے کے لئے نہیں کہے گا۔", "do_not_share_warning_text": "سپورٹ سمیت کسی اور کے ساتھ ان کا اشتراک نہ کریں۔\\n\\nآپ کے فنڈز چوری ہو سکتے ہیں اور ہو جائیں گے!", "do_not_show_me": "مجھے یہ دوبارہ مت دکھانا", "domain_looks_up": "ڈومین تلاش کرنا", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index e13d79b6b..a5ef82d62 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -160,6 +160,7 @@ "contact_name": "Orúkọ olùbásọ̀rọ̀", "contact_support": "Bá ìranlọ́wọ́ sọ̀rọ̀", "continue_text": "Tẹ̀síwájú", + "contract_warning": "Adirẹsi adehun adehun yii ti samisi bi arekereke. Jọwọ ṣe ilana pẹlu iṣọra.", "contractName": "Orukọ adehun", "contractSymbol": "Aami adehun", "copied_key_to_clipboard": "Ti ṣeda ${key} sí àtẹ àkọsílẹ̀", @@ -219,6 +220,7 @@ "displayable": "A lè ṣàfihàn ẹ̀", "do_not_have_enough_gas_asset": "O ko ni to ${currency} lati ṣe idunadura kan pẹlu awọn ipo nẹtiwọki blockchain lọwọlọwọ. O nilo diẹ sii ${currency} lati san awọn owo nẹtiwọọki blockchain, paapaa ti o ba nfi dukia miiran ranṣẹ.", "do_not_send": "Ẹ kò ránṣ", + "do_not_send_funds_to_contract_address_warning": "Maṣe fi owo ranṣẹ si adirẹsi yii \n\n Eyi jẹ idamọ fun àmi, eyikeyi awọn owo ti a firanṣẹ si adirẹsi yii yoo ko beere lọwọ rẹ lati ṣafikun adirẹsi adehun kan", "do_not_share_warning_text": "Ẹ kò pín wọnyìí sí ẹnikẹ́ni. Ẹ sì kò pin wọnyìí sí ìranlọ́wọ́. Ẹnikẹ́ni lè jí owó yín! Wọ́n máa jí owó yín!", "do_not_show_me": "Kò fi eléyìí hàn mi mọ́", "domain_looks_up": "Awọn wiwa agbegbe", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index a26edec4e..37e760de2 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -160,6 +160,7 @@ "contact_name": "联系人姓名", "contact_support": "联系支持", "continue_text": "继续", + "contract_warning": "该合同地址已被标记为潜在的欺诈性。请谨慎处理。", "contractName": "合约名称", "contractSymbol": "合约符号", "copied_key_to_clipboard": "复制 ${key} 到剪贴板", @@ -219,6 +220,7 @@ "displayable": "可显示", "do_not_have_enough_gas_asset": "您没有足够的 ${currency} 来在当前的区块链网络条件下进行交易。即使您发送的是不同的资产,您也需要更多的 ${currency} 来支付区块链网络费用。", "do_not_send": "不要发送", + "do_not_send_funds_to_contract_address_warning": "请勿将资金发送到此地址\n\n这只是令牌的标识符,任何发送到此地址的资金都将丢失。\n\n注意:蛋糕永远不会要求您添加合同地址", "do_not_share_warning_text": "请勿与其他任何人分享这些信息,包括支持人员。\n\n您的资金可能而且将会被盗!", "do_not_show_me": "不再提示", "domain_looks_up": "域名查找", diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 96787a403..3084af07c 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -43,6 +43,8 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('etherScanApiKey', () => ''), + SecretKey('polygonScanApiKey', () => ''), SecretKey('letsExchangeBearerToken', () => ''), SecretKey('letsExchangeAffiliateId', () => ''), SecretKey('stealthExBearerToken', () => ''), From 417de3669c4e37720b5a4a7f6d4484d2770cc10f Mon Sep 17 00:00:00 2001 From: cyan Date: Sun, 15 Sep 2024 17:48:07 +0200 Subject: [PATCH 079/142] do not restore deterministic wallet if it's not one (#1676) * do not restore deterministic wallet if it's not one * [skip ci] update comment --- cw_monero/lib/api/wallet_manager.dart | 28 +++++++++++++++++++++++++- cw_wownero/lib/api/wallet_manager.dart | 28 ++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index f06fe3e66..dca7586fb 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -123,7 +123,7 @@ void restoreWalletFromKeysSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - final newWptr = spendKey != "" + var newWptr = (spendKey != "") ? monero.WalletManager_createDeterministicWalletFromSpendKey( wmPtr, path: path, @@ -149,6 +149,32 @@ void restoreWalletFromKeysSync( message: monero.Wallet_errorString(newWptr)); } + // 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); + if (viewKey != viewKeyRestored && viewKey != "") { + monero.WalletManager_closeWallet(wmPtr, newWptr, false); + File(path).deleteSync(); + File(path+".keys").deleteSync(); + newWptr = monero.WalletManager_createWalletFromKeys( + wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + addressString: address, + viewKeyString: viewKey, + spendKeyString: spendKey, + nettype: 0, + ); + final status = monero.Wallet_status(newWptr); + if (status != 0) { + throw WalletRestoreFromKeysException( + message: monero.Wallet_errorString(newWptr)); + } + } + } + wptr = newWptr; openedWalletsByPath[path] = wptr!; diff --git a/cw_wownero/lib/api/wallet_manager.dart b/cw_wownero/lib/api/wallet_manager.dart index 660433ba6..ed6d86823 100644 --- a/cw_wownero/lib/api/wallet_manager.dart +++ b/cw_wownero/lib/api/wallet_manager.dart @@ -140,7 +140,7 @@ void restoreWalletFromKeysSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - final newWptr = spendKey != "" + var newWptr = (spendKey != "") ? wownero.WalletManager_createDeterministicWalletFromSpendKey( wmPtr, path: path, @@ -165,7 +165,31 @@ void restoreWalletFromKeysSync( throw WalletRestoreFromKeysException( message: wownero.Wallet_errorString(newWptr)); } - + // CW-712 - Try to restore deterministic wallet first, if the view key doesn't + // match the view key provided + if (spendKey != "") { + final viewKeyRestored = wownero.Wallet_secretViewKey(newWptr); + if (viewKey != viewKeyRestored && viewKey != "") { + wownero.WalletManager_closeWallet(wmPtr, newWptr, false); + File(path).deleteSync(); + File(path+".keys").deleteSync(); + newWptr = wownero.WalletManager_createWalletFromKeys( + wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + addressString: address, + viewKeyString: viewKey, + spendKeyString: spendKey, + nettype: 0, + ); + final status = wownero.Wallet_status(newWptr); + if (status != 0) { + throw WalletRestoreFromKeysException( + message: wownero.Wallet_errorString(newWptr)); + } + } + } wptr = newWptr; openedWalletsByPath[path] = wptr!; From c6a4c69084baa5cc8feadd967afb6d9a945acaf4 Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 18 Sep 2024 01:50:31 +0300 Subject: [PATCH 080/142] CW-719 Transaction dates shows incorrectly (#1685) * ignore 0 height * fix date format --- cw_bitcoin/lib/electrum_transaction_history.dart | 9 +++++++-- cw_bitcoin/lib/electrum_wallet.dart | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 806f813dd..b688f097b 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -76,8 +76,13 @@ abstract class ElectrumTransactionHistoryBase final val = entry.value; if (val is Map) { - final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type); - _update(tx); + // removing transactions with invalid date + if (val['date'] == 1168650000) { + transactions.remove(entry.key); + } else { + final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type); + _update(tx); + } } }); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 191272b1f..fe8782adb 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1613,7 +1613,7 @@ abstract class ElectrumWalletBase } if (height != null) { - if (time == null) { + if (time == null && height > 0) { time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); } From b6bc3df62276b046104842617cc497259aa15061 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Wed, 18 Sep 2024 09:22:36 -0700 Subject: [PATCH 081/142] Fix list UI having double width on some parts (#1687) * save * fix * re-add support button * update --- .../screens/dashboard/widgets/menu_widget.dart | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 78d8abc95..1caeb4c51 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -97,7 +97,15 @@ class MenuWidgetState extends State { @override Widget build(BuildContext context) { - final itemCount = SettingActions.all.length; + List items = SettingActions.all; + if (!widget.dashboardViewModel.hasSilentPayments) { + items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings); + } + // if (!widget.dashboardViewModel.hasMweb) { + // itemCount--; + // items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings); + // } + int itemCount = items.length; moneroIcon = Image.asset('assets/images/monero_menu.png', color: Theme.of(context).extension()!.iconColor); @@ -181,12 +189,7 @@ class MenuWidgetState extends State { index--; - final item = SettingActions.all[index]; - - if (!widget.dashboardViewModel.hasSilentPayments && - item.name(context) == S.of(context).silent_payments_settings) { - return Container(); - } + final item = items[index]; final isLastTile = index == itemCount - 1; From 3a391f10a37e4f8726fb6d374fe3aa217a78a1b7 Mon Sep 17 00:00:00 2001 From: Rafael Date: Fri, 20 Sep 2024 14:24:25 +0000 Subject: [PATCH 082/142] Sp enhancements (#1672) * fix: enhance regex, fix multiline * feat: improve scan msg, fix missing txs, use date api * feat: node fixes, enhance send modal, TX list tag & filter, refactors * fix: continuous scanning * fix: missing close * fix: resubscribe tweaks * feat: use mempool api setting toggle * handle any failure of height API and fallback to the old method [skip ci] --------- Co-authored-by: OmarHatem --- cw_bitcoin/lib/address_to_output_script.dart | 14 -- cw_bitcoin/lib/bitcoin_address_record.dart | 7 +- cw_bitcoin/lib/electrum.dart | 58 ++--- cw_bitcoin/lib/electrum_transaction_info.dart | 32 ++- cw_bitcoin/lib/electrum_wallet.dart | 201 +++++++++------- cw_bitcoin/lib/litecoin_wallet.dart | 2 +- .../lib/pending_bitcoin_transaction.dart | 13 + cw_bitcoin/lib/script_hash.dart | 19 -- cw_bitcoin/pubspec.lock | 4 +- cw_bitcoin/pubspec.yaml | 2 +- cw_bitcoin_cash/pubspec.yaml | 2 +- cw_core/lib/get_height_by_date.dart | 11 +- cw_core/lib/pending_transaction.dart | 8 + cw_core/lib/sync_status.dart | 8 + cw_nano/pubspec.lock | 4 +- lib/bitcoin/cw_bitcoin.dart | 48 +++- lib/core/address_validator.dart | 123 +++++----- lib/core/sync_status_title.dart | 6 +- lib/entities/parse_address_from_domain.dart | 5 +- .../dashboard/pages/transactions_page.dart | 12 +- .../dashboard/widgets/transaction_raw.dart | 126 ++++++---- lib/src/screens/rescan/rescan_page.dart | 1 + lib/src/screens/send/send_page.dart | 50 ++-- .../send/widgets/confirm_sending_alert.dart | 223 +++++++++++------- lib/src/screens/settings/privacy_page.dart | 26 +- lib/src/widgets/blockchain_height_widget.dart | 14 +- .../dashboard/transaction_filter_store.dart | 59 +++-- .../dashboard/dashboard_view_model.dart | 14 +- lib/view_model/rescan_view_model.dart | 4 + lib/view_model/send/output.dart | 19 +- lib/view_model/send/send_view_model.dart | 11 +- pubspec_base.yaml | 4 +- res/values/strings_ar.arb | 9 +- res/values/strings_bg.arb | 9 +- res/values/strings_cs.arb | 9 +- res/values/strings_de.arb | 9 +- res/values/strings_en.arb | 9 +- res/values/strings_es.arb | 9 +- 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 | 9 +- res/values/strings_my.arb | 9 +- res/values/strings_nl.arb | 9 +- res/values/strings_pl.arb | 9 +- res/values/strings_pt.arb | 13 +- 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 | 19 +- res/values/strings_yo.arb | 9 +- res/values/strings_zh.arb | 9 +- tool/configure.dart | 7 +- 61 files changed, 872 insertions(+), 531 deletions(-) delete mode 100644 cw_bitcoin/lib/address_to_output_script.dart delete mode 100644 cw_bitcoin/lib/script_hash.dart diff --git a/cw_bitcoin/lib/address_to_output_script.dart b/cw_bitcoin/lib/address_to_output_script.dart deleted file mode 100644 index 892f7a0d6..000000000 --- a/cw_bitcoin/lib/address_to_output_script.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:typed_data'; -import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin; - -List addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) { - try { - if (network == bitcoin.BitcoinCashNetwork.mainnet) { - return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes(); - } - return bitcoin.addressToOutputScript(address: address, network: network); - } catch (err) { - print(err); - return Uint8List(0); - } -} diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index bf36e6fb9..7e4b5f58f 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/script_hash.dart' as sh; abstract class BaseBitcoinAddressRecord { BaseBitcoinAddressRecord( @@ -65,8 +64,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { required super.type, String? scriptHash, required super.network, - }) : scriptHash = - scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null); + }) : scriptHash = scriptHash ?? + (network != null ? BitcoinAddressUtils.scriptHash(address, network: network) : null); factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { final decoded = json.decode(jsonSource) as Map; @@ -92,7 +91,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { String getScriptHash(BasedUtxoNetwork network) { if (scriptHash != null) return scriptHash!; - scriptHash = sh.scriptHash(address, network: network); + scriptHash = BitcoinAddressUtils.scriptHash(address, network: network); return scriptHash!; } diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 8dfb8e01f..d8cca5b16 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; @@ -48,6 +47,7 @@ class ElectrumClient { final Map _tasks; Map get tasks => _tasks; final Map _errors; + ConnectionStatus _connectionStatus = ConnectionStatus.disconnected; bool _isConnected; Timer? _aliveTimer; String unterminatedString; @@ -57,11 +57,13 @@ class ElectrumClient { Future connectToUri(Uri uri, {bool? useSSL}) async { this.uri = uri; - this.useSSL = useSSL; - await connect(host: uri.host, port: uri.port, useSSL: useSSL); + if (useSSL != null) { + this.useSSL = useSSL; + } + await connect(host: uri.host, port: uri.port); } - Future connect({required String host, required int port, bool? useSSL}) async { + Future connect({required String host, required int port}) async { _setConnectionStatus(ConnectionStatus.connecting); try { @@ -80,15 +82,26 @@ class ElectrumClient { onBadCertificate: (_) => true, ); } - } catch (_) { - _setConnectionStatus(ConnectionStatus.failed); + } catch (e) { + if (e is HandshakeException) { + useSSL = !(useSSL ?? false); + } + + if (_connectionStatus != ConnectionStatus.connecting) { + _setConnectionStatus(ConnectionStatus.failed); + } + return; } if (socket == null) { - _setConnectionStatus(ConnectionStatus.failed); + if (_connectionStatus != ConnectionStatus.connecting) { + _setConnectionStatus(ConnectionStatus.failed); + } + return; } + _setConnectionStatus(ConnectionStatus.connected); socket!.listen( @@ -118,7 +131,7 @@ class ElectrumClient { socket?.destroy(); _setConnectionStatus(ConnectionStatus.disconnected); } - } catch(e) { + } catch (e) { print(e.toString()); } }, @@ -217,25 +230,6 @@ class ElectrumClient { return []; }); - Future>> getListUnspentWithAddress( - String address, BasedUtxoNetwork network) => - call( - method: 'blockchain.scripthash.listunspent', - params: [scriptHash(address, network: network)]).then((dynamic result) { - if (result is List) { - return result.map((dynamic val) { - if (val is Map) { - val['address'] = address; - return val; - } - - return {}; - }).toList(); - } - - return []; - }); - Future>> getListUnspent(String scriptHash) => call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]) .then((dynamic result) { @@ -272,16 +266,12 @@ class ElectrumClient { try { final result = await callWithTimeout( method: 'blockchain.transaction.get', params: [hash, verbose], timeout: 10000); - if (result is Map) { - return result; - } + return result; } on RequestFailedTimeoutException catch (_) { return {}; } catch (e) { - print("getTransaction: ${e.toString()}"); return {}; } - return {}; } Future> getTransactionVerbose({required String hash}) => @@ -326,9 +316,8 @@ class ElectrumClient { await call(method: 'blockchain.block.get_header', params: [height]) as Map; BehaviorSubject? tweaksSubscribe({required int height, required int count}) { - _id += 1; return subscribe( - id: 'blockchain.tweaks.subscribe:${height + count}', + id: 'blockchain.tweaks.subscribe', method: 'blockchain.tweaks.subscribe', params: [height, count, false], ); @@ -539,6 +528,7 @@ class ElectrumClient { void _setConnectionStatus(ConnectionStatus status) { onConnectionStatusChange?.call(status); + _connectionStatus = status; _isConnected = status == ConnectionStatus.connected; } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index ebd90ff2b..8b52b7aca 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -23,20 +23,24 @@ class ElectrumTransactionBundle { class ElectrumTransactionInfo extends TransactionInfo { List? unspents; + bool isReceivedSilentPayment; - ElectrumTransactionInfo(this.type, - {required String id, - int? height, - required int amount, - int? fee, - List? inputAddresses, - List? outputAddresses, - required TransactionDirection direction, - required bool isPending, - required DateTime date, - required int confirmations, - String? to, - this.unspents}) { + ElectrumTransactionInfo( + this.type, { + required String id, + int? height, + required int amount, + int? fee, + List? inputAddresses, + List? outputAddresses, + required TransactionDirection direction, + required bool isPending, + required DateTime date, + required int confirmations, + String? to, + this.unspents, + this.isReceivedSilentPayment = false, + }) { this.id = id; this.height = height; this.amount = amount; @@ -202,6 +206,7 @@ class ElectrumTransactionInfo extends TransactionInfo { .map((unspent) => BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map)) .toList(), + isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, ); } @@ -252,6 +257,7 @@ class ElectrumTransactionInfo extends TransactionInfo { m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? []; m['inputAddresses'] = inputAddresses; m['outputAddresses'] = outputAddresses; + m['isReceivedSilentPayment'] = isReceivedSilentPayment; return m; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index fe8782adb..5eb4bd85c 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -24,7 +24,6 @@ import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; -import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; @@ -51,8 +50,6 @@ part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -const int TWEAKS_COUNT = 25; - abstract class ElectrumWalletBase extends WalletBase with Store, WalletKeysFile { @@ -166,12 +163,12 @@ abstract class ElectrumWalletBase Set get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet(); List get scriptHashes => walletAddresses.addressesByReceiveType - .map((addr) => scriptHash(addr.address, network: network)) + .map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network)) .toList(); List get publicScriptHashes => walletAddresses.allAddresses .where((addr) => !addr.isHidden) - .map((addr) => scriptHash(addr.address, network: network)) + .map((addr) => addr.getScriptHash(network)) .toList(); String get xpub => accountHD.publicKey.toExtended; @@ -212,7 +209,7 @@ abstract class ElectrumWalletBase silentPaymentsScanningActive = active; if (active) { - syncStatus = StartingScanSyncStatus(); + syncStatus = AttemptingScanSyncStatus(); final tip = await getUpdatedChainTip(); @@ -290,12 +287,7 @@ abstract class ElectrumWalletBase } @action - Future _setListeners( - int height, { - int? chainTipParam, - bool? doSingleScan, - bool? usingSupportedNode, - }) async { + Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { final chainTip = chainTipParam ?? await getUpdatedChainTip(); if (chainTip == height) { @@ -303,7 +295,7 @@ abstract class ElectrumWalletBase return; } - syncStatus = StartingScanSyncStatus(); + syncStatus = AttemptingScanSyncStatus(); if (_isolate != null) { final runningIsolate = await _isolate!; @@ -550,7 +542,8 @@ abstract class ElectrumWalletBase electrumClient.onConnectionStatusChange = _onConnectionStatusChange; await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); - } catch (e) { + } catch (e, stacktrace) { + print(stacktrace); print(e.toString()); syncStatus = FailedSyncStatus(); } @@ -592,7 +585,7 @@ abstract class ElectrumWalletBase allInputsAmount += utx.value; leftAmount = leftAmount - utx.value; - final address = addressTypeFromStr(utx.address, network); + final address = RegexUtils.addressTypeFromStr(utx.address, network); ECPrivate? privkey; bool? isSilentPayment = false; @@ -796,10 +789,11 @@ abstract class ElectrumWalletBase } final changeAddress = await walletAddresses.getChangeAddress(); - final address = addressTypeFromStr(changeAddress, network); + final address = RegexUtils.addressTypeFromStr(changeAddress, network); outputs.add(BitcoinOutput( address: address, value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, )); int estimatedSize; @@ -833,8 +827,12 @@ abstract class ElectrumWalletBase if (!_isBelowDust(amountLeftForChange)) { // Here, lastOutput already is change, return the amount left without the fee to the user's address. - outputs[outputs.length - 1] = - BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange)); + outputs[outputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); } else { // If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change outputs.removeLast(); @@ -938,18 +936,27 @@ abstract class ElectrumWalletBase credentialsAmount += outputAmount; - final address = - addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network); + final address = RegexUtils.addressTypeFromStr( + out.isParsedAddress ? out.extractedAddress! : out.address, network); + final isSilentPayment = address is SilentPaymentAddress; - if (address is SilentPaymentAddress) { + if (isSilentPayment) { hasSilentPayment = true; } if (sendAll) { // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent - outputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(0), + isSilentPayment: isSilentPayment, + )); } else { - outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount))); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(outputAmount), + isSilentPayment: isSilentPayment, + )); } } @@ -1089,7 +1096,8 @@ abstract class ElectrumWalletBase }); } - unspentCoins.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); + unspentCoins + .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); await updateBalance(); }); @@ -1237,12 +1245,7 @@ abstract class ElectrumWalletBase @action @override - Future rescan({ - required int height, - int? chainTip, - ScanData? scanData, - bool? doSingleScan, - }) async { + Future rescan({required int height, bool? doSingleScan}) async { silentPaymentsScanningActive = true; _setListeners(height, doSingleScan: doSingleScan); } @@ -1460,7 +1463,7 @@ abstract class ElectrumWalletBase final addressRecord = walletAddresses.allAddresses.firstWhere((element) => element.address == address); - final btcAddress = addressTypeFromStr(addressRecord.address, network); + final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); final privkey = generateECPrivate( hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, index: addressRecord.index, @@ -1501,7 +1504,7 @@ abstract class ElectrumWalletBase } final address = addressFromOutputScript(out.scriptPubKey, network); - final btcAddress = addressTypeFromStr(address, network); + final btcAddress = RegexUtils.addressTypeFromStr(address, network); outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt()))); } @@ -1597,8 +1600,6 @@ abstract class ElectrumWalletBase Future getTransactionExpanded( {required String hash, int? height}) async { String transactionHex; - // TODO: time is not always available, and calculating it from height is not always accurate. - // Add settings to choose API provider and use and http server instead of electrum for this. int? time; int? confirmations; @@ -1606,6 +1607,29 @@ abstract class ElectrumWalletBase if (verboseTransaction.isEmpty) { transactionHex = await electrumClient.getTransactionHex(hash: hash); + + if (height != null && await checkIfMempoolAPIIsEnabled()) { + final blockHash = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", + ), + ); + + if (blockHash.statusCode == 200 && + blockHash.body.isNotEmpty && + jsonDecode(blockHash.body) != null) { + final blockResponse = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + ), + ); + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + } + } + } } else { transactionHex = verboseTransaction['hex'] as String; time = verboseTransaction['time'] as int?; @@ -1860,7 +1884,7 @@ abstract class ElectrumWalletBase final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; - final sh = scriptHash(addressRecord.address, network: network); + final sh = addressRecord.getScriptHash(network); final balanceFuture = electrumClient.getBalance(sh); balanceFutures.add(balanceFuture); } @@ -1900,7 +1924,10 @@ abstract class ElectrumWalletBase } return ElectrumBalance( - confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen); + confirmed: totalConfirmed, + unconfirmed: totalUnconfirmed, + frozen: totalFrozen, + ); } Future updateBalance() async { @@ -1968,7 +1995,7 @@ abstract class ElectrumWalletBase List possibleRecoverIds = [0, 1]; - final baseAddress = addressTypeFromStr(address, network); + final baseAddress = RegexUtils.addressTypeFromStr(address, network); for (int recoveryId in possibleRecoverIds) { final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId); @@ -2061,7 +2088,8 @@ abstract class ElectrumWalletBase _isTryingToConnect = true; Timer(Duration(seconds: 5), () { - if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) { + if (this.syncStatus is NotConnectedSyncStatus || + this.syncStatus is LostConnectionSyncStatus) { this.electrumClient.connectToUri( node!.uri, useSSL: node!.useSSL ?? false, @@ -2192,21 +2220,22 @@ Future startRefresh(ScanData scanData) async { BehaviorSubject? tweaksSubscription = null; - final syncingStatus = scanData.isSingleScan - ? SyncingSyncStatus(1, 0) - : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); - - // Initial status UI update, send how many blocks left to scan - scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - final electrumClient = scanData.electrumClient; await electrumClient.connectToUri( scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"), useSSL: scanData.node?.useSSL ?? false, ); + int getCountPerRequest(int syncHeight) { + if (scanData.isSingleScan) { + return 1; + } + + final amountLeft = scanData.chainTip - syncHeight + 1; + return amountLeft; + } + if (tweaksSubscription == null) { - final count = scanData.isSingleScan ? 1 : TWEAKS_COUNT; final receiver = Receiver( scanData.silentAddress.b_scan.toHex(), scanData.silentAddress.B_spend.toHex(), @@ -2215,16 +2244,45 @@ Future startRefresh(ScanData scanData) async { scanData.labelIndexes.length, ); - tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight, count: count); - tweaksSubscription?.listen((t) async { - final tweaks = t as Map; + // Initial status UI update, send how many blocks in total to scan + final initialCount = getCountPerRequest(syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); - if (tweaks["message"] != null) { + tweaksSubscription = await electrumClient.tweaksSubscribe( + height: syncHeight, + count: initialCount, + ); + + Future listenFn(t) async { + final tweaks = t as Map; + final msg = tweaks["message"]; + // success or error msg + final noData = msg != null; + + if (noData) { // re-subscribe to continue receiving messages, starting from the next unscanned height - electrumClient.tweaksSubscribe(height: syncHeight + 1, count: count); + final nextHeight = syncHeight + 1; + final nextCount = getCountPerRequest(nextHeight); + + if (nextCount > 0) { + tweaksSubscription?.close(); + + final nextTweaksSubscription = electrumClient.tweaksSubscribe( + height: nextHeight, + count: nextCount, + ); + nextTweaksSubscription?.listen(listenFn); + } + return; } + // Continuous status UI update, send how many blocks left to scan + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + final blockHeight = tweaks.keys.first; final tweakHeight = int.parse(blockHeight); @@ -2264,6 +2322,7 @@ Future startRefresh(ScanData scanData) async { : DateTime.now(), confirmations: scanData.chainTip - tweakHeight + 1, unspents: [], + isReceivedSilentPayment: true, ); addToWallet.forEach((label, value) { @@ -2318,16 +2377,6 @@ Future startRefresh(ScanData scanData) async { } catch (_) {} syncHeight = tweakHeight; - scanData.sendPort.send( - SyncResponse( - syncHeight, - SyncingSyncStatus.fromHeightValues( - scanData.chainTip, - initialSyncHeight, - syncHeight, - ), - ), - ); if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { if (tweakHeight >= scanData.chainTip) @@ -2343,7 +2392,9 @@ Future startRefresh(ScanData scanData) async { await tweaksSubscription!.close(); await electrumClient.close(); } - }); + } + + tweaksSubscription?.listen(listenFn); } if (tweaksSubscription == null) { @@ -2373,6 +2424,7 @@ class EstimatedTxResult { final int fee; final int amount; final bool spendsSilentPayment; + // final bool sendsToSilentPayment; final bool hasChange; final bool isSendAll; final String? memo; @@ -2386,31 +2438,6 @@ class PublicKeyWithDerivationPath { final String publicKey; } -BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) { - if (network is BitcoinCashNetwork) { - if (!address.startsWith("bitcoincash:") && - (address.startsWith("q") || address.startsWith("p"))) { - address = "bitcoincash:$address"; - } - - return BitcoinCashAddress(address).baseAddress; - } - - if (P2pkhAddress.regex.hasMatch(address)) { - return P2pkhAddress.fromAddress(address: address, network: network); - } else if (P2shAddress.regex.hasMatch(address)) { - return P2shAddress.fromAddress(address: address, network: network); - } else if (P2wshAddress.regex.hasMatch(address)) { - return P2wshAddress.fromAddress(address: address, network: network); - } else if (P2trAddress.regex.hasMatch(address)) { - return P2trAddress.fromAddress(address: address, network: network); - } else if (SilentPaymentAddress.regex.hasMatch(address)) { - return SilentPaymentAddress.fromAddress(address); - } else { - return P2wpkhAddress.fromAddress(address: address, network: network); - } -} - BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { if (type is P2pkhAddress) { return P2pkhAddressType.p2pkh; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 12a43dbe6..2e4683ae6 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -301,7 +301,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { List possibleRecoverIds = [0, 1]; - final baseAddress = addressTypeFromStr(address, network); + final baseAddress = RegexUtils.addressTypeFromStr(address, network); for (int recoveryId in possibleRecoverIds) { final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 6a4cd1741..26ed3a4be 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -47,6 +47,19 @@ class PendingBitcoinTransaction with PendingTransaction { @override int? get outputCount => _tx.outputs.length; + List get outputs => _tx.outputs; + + bool get hasSilentPayment => _tx.hasSilentPayment; + + PendingChange? get change { + try { + final change = _tx.outputs.firstWhere((out) => out.isChange); + return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); + } catch (_) { + return null; + } + } + final List _listeners; @override diff --git a/cw_bitcoin/lib/script_hash.dart b/cw_bitcoin/lib/script_hash.dart deleted file mode 100644 index 2130fcbbe..000000000 --- a/cw_bitcoin/lib/script_hash.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:crypto/crypto.dart'; -import 'package:cw_bitcoin/address_to_output_script.dart'; -import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin; - -String scriptHash(String address, {required bitcoin.BasedUtxoNetwork network}) { - final outputScript = addressToOutputScript(address, network); - final parts = sha256.convert(outputScript).toString().split(''); - var res = ''; - - for (var i = parts.length - 1; i >= 0; i--) { - final char = parts[i]; - i--; - final nextChar = parts[i]; - res += nextChar; - res += char; - } - - return res; -} diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 301b2aeff..a2478af3c 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -70,8 +70,8 @@ packages: dependency: "direct overridden" description: path: "." - ref: cake-update-v5 - resolved-ref: ff2b10eb27b0254ce4518d054332d97d77d9b380 + ref: cake-update-v7 + resolved-ref: bc49e3b1cba601828f8ddc3d016188d8c2499088 url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index d085d98e3..d3fe2ee4e 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -57,7 +57,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v5 + ref: cake-update-v7 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 64bd38b1d..a3b113d9f 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -42,7 +42,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v5 + ref: cake-update-v7 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 204f03d62..10a2aeab5 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -267,6 +267,16 @@ const bitcoinDates = { "2023-01": 769810, }; +Future getBitcoinHeightByDateAPI({required DateTime date}) async { + final response = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}", + ), + ); + + return jsonDecode(response.body)['height'] as int; +} + int getBitcoinHeightByDate({required DateTime date}) { String dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}'; final closestKey = bitcoinDates.keys @@ -377,4 +387,3 @@ int getWowneroHeightByDate({required DateTime date}) { return wowDates[closestKey] ?? 0; } - diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index 642db9c2c..0a6103a5f 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -1,3 +1,10 @@ +class PendingChange { + final String address; + final String amount; + + PendingChange(this.address, this.amount); +} + mixin PendingTransaction { String get id; String get amountFormatted; @@ -5,6 +12,7 @@ mixin PendingTransaction { String? feeRate; String get hex; int? get outputCount => null; + PendingChange? change; Future commit(); } diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index ea015340c..788309d8c 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -4,6 +4,9 @@ abstract class SyncStatus { } class StartingScanSyncStatus extends SyncStatus { + StartingScanSyncStatus(this.beginHeight); + + final int beginHeight; @override double progress() => 0.0; } @@ -59,6 +62,11 @@ class AttemptingSyncStatus extends SyncStatus { double progress() => 0.0; } +class AttemptingScanSyncStatus extends SyncStatus { + @override + double progress() => 0.0; +} + class FailedSyncStatus extends NotConnectedSyncStatus {} class ConnectingSyncStatus extends SyncStatus { diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index ef9de14f9..e641024f7 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -117,10 +117,10 @@ packages: dependency: "direct overridden" description: name: build_runner_core - sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" url: "https://pub.dev" source: hosted - version: "7.2.7+1" + version: "7.2.7" built_collection: dependency: transitive description: diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index c016c1f2a..2ca74822a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -361,7 +361,7 @@ class CWBitcoin extends Bitcoin { continue; } - final sh = scriptHash(address, network: network); + final sh = BitcoinAddressUtils.scriptHash(address, network: network); final history = await electrumClient.getHistory(sh); final balance = await electrumClient.getBalance(sh); @@ -522,7 +522,20 @@ class CWBitcoin extends Bitcoin { } @override - int getHeightByDate({required DateTime date}) => getBitcoinHeightByDate(date: date); + Future checkIfMempoolAPIIsEnabled(Object wallet) async { + final bitcoinWallet = wallet as ElectrumWallet; + return await bitcoinWallet.checkIfMempoolAPIIsEnabled(); + } + + @override + Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}) async { + if (bitcoinMempoolAPIEnabled ?? false) { + try { + return await getBitcoinHeightByDateAPI(date: date); + } catch (_) {} + } + return await getBitcoinHeightByDate(date: date); + } @override Future rescan(Object wallet, {required int height, bool? doSingleScan}) async { @@ -547,4 +560,35 @@ class CWBitcoin extends Bitcoin { final bitcoinWallet = wallet as ElectrumWallet; await bitcoinWallet.updateFeeRates(); } + + @override + List updateOutputs(PendingTransaction pendingTransaction, List outputs) { + final pendingTx = pendingTransaction as PendingBitcoinTransaction; + + if (!pendingTx.hasSilentPayment) { + return outputs; + } + + final updatedOutputs = outputs.map((output) { + + try { + final pendingOut = pendingTx!.outputs[outputs.indexOf(output)]; + final updatedOutput = output; + + updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey) + .toAddress(BitcoinNetwork.mainnet); + return updatedOutput; + } catch (_) {} + + return output; + }).toList(); + + return updatedOutputs; + } + + @override + bool txIsReceivedSilentPayment(TransactionInfo txInfo) { + final tx = txInfo as ElectrumTransactionInfo; + return tx.isReceivedSilentPayment; + } } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 92a735481..42e55d2b7 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -5,32 +5,40 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; +const BEFORE_REGEX = '(^|\s)'; +const AFTER_REGEX = '(\$|\s)'; + class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) : super( errorMessage: S.current.error_text_address, useAdditionalValidation: type == CryptoCurrency.btc - ? (String txt) => validateAddress(address: txt, network: BitcoinNetwork.mainnet) + ? (String txt) => BitcoinAddressUtils.validateAddress( + address: txt, + network: BitcoinNetwork.mainnet, + ) : null, pattern: getPattern(type), length: getLength(type)); static String getPattern(CryptoCurrency type) { + var pattern = ""; if (type is Erc20Token) { - return '0x[0-9a-zA-Z]'; + pattern = '0x[0-9a-zA-Z]'; } switch (type) { case CryptoCurrency.xmr: - return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; + pattern = '4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}'; case CryptoCurrency.ada: - return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' - '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; + pattern = '[0-9a-zA-Z]{59}|[0-9a-zA-Z]{92}|[0-9a-zA-Z]{104}' + '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; case CryptoCurrency.btc: - return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${SilentPaymentAddress.regex.pattern}\$'; + pattern = + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.nano: - return '[0-9a-zA-Z_]'; + pattern = '[0-9a-zA-Z_]'; case CryptoCurrency.banano: - return '[0-9a-zA-Z_]'; + pattern = '[0-9a-zA-Z_]'; case CryptoCurrency.usdc: case CryptoCurrency.usdcpoly: case CryptoCurrency.usdtPoly: @@ -66,11 +74,11 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dydx: case CryptoCurrency.steth: case CryptoCurrency.shib: - return '0x[0-9a-zA-Z]'; + pattern = '0x[0-9a-zA-Z]'; case CryptoCurrency.xrp: - return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; + pattern = '[0-9a-zA-Z]{34}|X[0-9a-zA-Z]{46}'; case CryptoCurrency.xhv: - return '^hvx|hvi|hvs[0-9a-zA-Z]'; + pattern = 'hvx|hvi|hvs[0-9a-zA-Z]'; case CryptoCurrency.xag: case CryptoCurrency.xau: case CryptoCurrency.xaud: @@ -92,40 +100,43 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dash: case CryptoCurrency.eos: case CryptoCurrency.wow: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]'; case CryptoCurrency.bch: - return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{42}\$|^bitcoincash:q|p[0-9a-zA-Z]{41}\$|^bitcoincash:q|p[0-9a-zA-Z]{42}\$'; + pattern = + '(?!bitcoincash:)[0-9a-zA-Z]*|(?!bitcoincash:)q|p[0-9a-zA-Z]{41}|(?!bitcoincash:)q|p[0-9a-zA-Z]{42}|bitcoincash:q|p[0-9a-zA-Z]{41}|bitcoincash:q|p[0-9a-zA-Z]{42}'; case CryptoCurrency.bnb: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]'; case CryptoCurrency.ltc: - return '^(?!(ltc|LTC)1)[0-9a-zA-Z]*\$|(^LTC1[A-Z0-9]*\$)|(^ltc1[a-z0-9]*\$)'; + pattern = '(?!(ltc|LTC)1)[0-9a-zA-Z]*|(LTC1[A-Z0-9]*)|(ltc1[a-z0-9]*)'; case CryptoCurrency.hbar: - return '[0-9a-zA-Z.]'; + pattern = '[0-9a-zA-Z.]'; case CryptoCurrency.zaddr: - return '^zs[0-9a-zA-Z]{75}'; + pattern = 'zs[0-9a-zA-Z]{75}'; case CryptoCurrency.zec: - return '^t1[0-9a-zA-Z]{33}\$|^t3[0-9a-zA-Z]{33}\$'; + pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; case CryptoCurrency.dcr: - return 'D[ksecS]([0-9a-zA-Z])+'; + pattern = 'D[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: - return '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; + pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.near: - return '[0-9a-f]{64}'; + pattern = '[0-9a-f]{64}'; case CryptoCurrency.rune: - return 'thor1[0-9a-z]{38}'; + pattern = 'thor1[0-9a-z]{38}'; case CryptoCurrency.scrt: - return 'secret1[0-9a-z]{38}'; + pattern = 'secret1[0-9a-z]{38}'; case CryptoCurrency.stx: - return 'S[MP][0-9a-zA-Z]+'; + pattern = 'S[MP][0-9a-zA-Z]+'; case CryptoCurrency.kmd: - return 'R[0-9a-zA-Z]{33}'; + pattern = 'R[0-9a-zA-Z]{33}'; case CryptoCurrency.pivx: - return 'D([1-9a-km-zA-HJ-NP-Z]){33}'; + pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.btcln: - return '^(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; + pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; default: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]'; } + + return '$BEFORE_REGEX$pattern$AFTER_REGEX'; } static List? getLength(CryptoCurrency type) { @@ -266,56 +277,54 @@ class AddressValidator extends TextValidator { } static String? getAddressFromStringPattern(CryptoCurrency type) { + String? pattern = null; + switch (type) { case CryptoCurrency.xmr: case CryptoCurrency.wow: - return '([^0-9a-zA-Z]|^)4[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; + pattern = '4[0-9a-zA-Z]{94}' + '|8[0-9a-zA-Z]{94}' + '|[0-9a-zA-Z]{106}'; case CryptoCurrency.btc: - return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type - '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)' //P2trAddress type - '|${SilentPaymentAddress.regex.pattern}\$'; - + pattern = + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: - return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; + pattern = '^L[a-zA-Z0-9]{26,33}' + '|[LM][a-km-zA-HJ-NP-Z1-9]{26,33}' + '|ltc[a-zA-Z0-9]{26,45}'; case CryptoCurrency.eth: - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; case CryptoCurrency.maticpoly: - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; case CryptoCurrency.nano: - return 'nano_[0-9a-zA-Z]{60}'; + pattern = 'nano_[0-9a-zA-Z]{60}'; case CryptoCurrency.banano: - return 'ban_[0-9a-zA-Z]{60}'; + pattern = 'ban_[0-9a-zA-Z]{60}'; case CryptoCurrency.bch: - return 'bitcoincash:q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' - '|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'; + pattern = '(bitcoincash:)?q[0-9a-zA-Z]{41,42}'; case CryptoCurrency.sol: - return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}'; case CryptoCurrency.trx: - return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; default: if (type.tag == CryptoCurrency.eth.title) { - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.maticpoly.tag) { - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.sol.title) { - return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}'; } if (type.tag == CryptoCurrency.trx.title) { - return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; } - - return null; } + + if (pattern != null) { + return "$BEFORE_REGEX$pattern$AFTER_REGEX"; + } + + return null; } } diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 465211f23..4582f7b1f 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -53,7 +53,11 @@ String syncStatusTitle(SyncStatus syncStatus) { } if (syncStatus is StartingScanSyncStatus) { - return S.current.sync_status_starting_scan; + return S.current.sync_status_starting_scan(syncStatus.beginHeight.toString()); + } + + if (syncStatus is AttemptingScanSyncStatus) { + return S.current.sync_status_attempting_scan; } return ''; diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index c95ce9847..481db5620 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -51,7 +51,7 @@ class AddressResolver { throw Exception('Unexpected token: $type for getAddressFromStringPattern'); } - final match = RegExp(addressPattern).firstMatch(raw); + final match = RegExp(addressPattern, multiLine: true).firstMatch(raw); return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), (Match match) { String group = match.group(0)!; @@ -213,8 +213,7 @@ class AddressResolver { await NostrProfileHandler.processRelays(context, nostrProfile!, text); if (nostrUserData != null) { - String? addressFromBio = extractAddressByType( - raw: nostrUserData.about, type: currency); + String? addressFromBio = extractAddressByType(raw: nostrUserData.about, type: currency); if (addressFromBio != null) { return ParsedAddress.nostrAddress( address: addressFromBio, diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 7c0e9cad4..a9a1213ce 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart'; import 'package:cake_wallet/themes/extensions/placeholder_theme.dart'; @@ -52,7 +53,7 @@ class TransactionsPage extends StatelessWidget { try { final uri = Uri.parse( "https://guides.cakewallet.com/docs/FAQ/why_are_my_funds_not_appearing/"); - launchUrl(uri, mode: LaunchMode.externalApplication); + launchUrl(uri, mode: LaunchMode.externalApplication); } catch (_) {} }, title: S.of(context).syncing_wallet_alert_title, @@ -84,7 +85,7 @@ class TransactionsPage extends StatelessWidget { final transaction = item.transaction; final transactionType = dashboardViewModel.type == WalletType.ethereum && - transaction.evmSignatureName == 'approval' + transaction.evmSignatureName == 'approval' ? ' (${transaction.evmSignatureName})' : ''; @@ -100,8 +101,11 @@ class TransactionsPage extends StatelessWidget { ? '' : item.formattedFiatAmount, isPending: transaction.isPending, - title: item.formattedTitle + - item.formattedStatus + ' $transactionType', + title: + item.formattedTitle + item.formattedStatus + ' $transactionType', + isReceivedSilentPayment: + dashboardViewModel.type == WalletType.bitcoin && + bitcoin!.txIsReceivedSilentPayment(transaction), ), ); } diff --git a/lib/src/screens/dashboard/widgets/transaction_raw.dart b/lib/src/screens/dashboard/widgets/transaction_raw.dart index 3a95b9f2e..88866fa25 100644 --- a/lib/src/screens/dashboard/widgets/transaction_raw.dart +++ b/lib/src/screens/dashboard/widgets/transaction_raw.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/transaction_direction.dart'; @@ -5,14 +6,16 @@ import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; class TransactionRow extends StatelessWidget { - TransactionRow( - {required this.direction, - required this.formattedDate, - required this.formattedAmount, - required this.formattedFiatAmount, - required this.isPending, - required this.title, - required this.onTap}); + TransactionRow({ + required this.direction, + required this.formattedDate, + required this.formattedAmount, + required this.formattedFiatAmount, + required this.isPending, + required this.isReceivedSilentPayment, + required this.title, + required this.onTap, + }); final VoidCallback onTap; final TransactionDirection direction; @@ -20,6 +23,7 @@ class TransactionRow extends StatelessWidget { final String formattedAmount; final String formattedFiatAmount; final bool isPending; + final bool isReceivedSilentPayment; final String title; @override @@ -38,50 +42,80 @@ class TransactionRow extends StatelessWidget { width: 36, decoration: BoxDecoration( shape: BoxShape.circle, - color: Theme.of(context).extension()!.rowsColor - ), - child: Image.asset( - direction == TransactionDirection.incoming - ? 'assets/images/down_arrow.png' - : 'assets/images/up_arrow.png'), + color: Theme.of(context).extension()!.rowsColor), + child: Image.asset(direction == TransactionDirection.incoming + ? 'assets/images/down_arrow.png' + : 'assets/images/up_arrow.png'), ), SizedBox(width: 12), Expanded( child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor)), - Text(formattedAmount, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor)) - ]), - SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(formattedDate, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.dateSectionRowColor)), - Text(formattedFiatAmount, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.dateSectionRowColor)) - ]) - ], - ) - ) + mainAxisSize: MainAxisSize.min, + children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Row( + children: [ + Text(title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor, + )), + if (isReceivedSilentPayment) TxTag(tag: S.of(context).silent_payment), + ], + ), + Text(formattedAmount, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor)) + ]), + SizedBox(height: 5), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(formattedDate, + style: TextStyle( + fontSize: 14, + color: + Theme.of(context).extension()!.dateSectionRowColor)), + Text(formattedFiatAmount, + style: TextStyle( + fontSize: 14, + color: + Theme.of(context).extension()!.dateSectionRowColor)) + ]) + ], + )) ], ), )); } } + +// A tag to add context to a transaction +// example use: differ silent payments from regular txs +class TxTag extends StatelessWidget { + TxTag({required this.tag}); + + final String tag; + + @override + Widget build(BuildContext context) { + return Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Theme.of(context).extension()!.rowsColor, + ), + alignment: Alignment.center, + child: Text( + tag.toLowerCase(), + style: TextStyle( + color: Theme.of(context).extension()!.textColor, + fontSize: 9, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index b66c94878..58e7a44f6 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -37,6 +37,7 @@ class RescanPage extends BasePage { toggleSingleScan: () => _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, walletType: _rescanViewModel.wallet.type, + bitcoinMempoolAPIEnabled: _rescanViewModel.isBitcoinMempoolAPIEnabled, )), Observer( builder: (_) => LoadingPrimaryButton( diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index be2e6cb02..3e906048d 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -68,11 +68,11 @@ class SendPage extends BasePage { @override Function(BuildContext)? get pushToNextWidget => (context) { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.focusedChild?.unfocus(); - } - }; + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; @override Widget? leading(BuildContext context) { @@ -212,26 +212,25 @@ class SendPage extends BasePage { final count = sendViewModel.outputs.length; return count > 1 - ? Semantics ( - label: 'Page Indicator', - hint: 'Swipe to change receiver', - excludeSemantics: true, - child: - SmoothPageIndicator( - controller: controller, - count: count, - effect: ScrollingDotsEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context) - .extension()! - .indicatorDotColor, - activeDotColor: Theme.of(context) - .extension()! - .templateBackgroundColor), - )) + ? Semantics( + label: 'Page Indicator', + hint: 'Swipe to change receiver', + excludeSemantics: true, + child: SmoothPageIndicator( + controller: controller, + count: count, + effect: ScrollingDotsEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context) + .extension()! + .indicatorDotColor, + activeDotColor: Theme.of(context) + .extension()! + .templateBackgroundColor), + )) : Offstage(); }, ), @@ -478,6 +477,7 @@ class SendPage extends BasePage { feeValue: sendViewModel.pendingTransaction!.feeFormatted, feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, outputs: sendViewModel.outputs, + change: sendViewModel.pendingTransaction!.change, rightButtonText: S.of(_dialogContext).send, leftButtonText: S.of(_dialogContext).cancel, actionRightButton: () async { diff --git a/lib/src/screens/send/widgets/confirm_sending_alert.dart b/lib/src/screens/send/widgets/confirm_sending_alert.dart index 3af1c3f8c..c7b6d3407 100644 --- a/lib/src/screens/send/widgets/confirm_sending_alert.dart +++ b/lib/src/screens/send/widgets/confirm_sending_alert.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -21,6 +22,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { required this.feeValue, required this.feeFiatAmount, required this.outputs, + this.change, required this.leftButtonText, required this.rightButtonText, required this.actionLeftButton, @@ -44,6 +46,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { final String feeValue; final String feeFiatAmount; final List outputs; + final PendingChange? change; final String leftButtonText; final String rightButtonText; final VoidCallback actionLeftButton; @@ -101,6 +104,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { feeValue: feeValue, feeFiatAmount: feeFiatAmount, outputs: outputs, + change: change, onDispose: onDispose); } @@ -117,6 +121,7 @@ class ConfirmSendingAlertContent extends StatefulWidget { required this.feeValue, required this.feeFiatAmount, required this.outputs, + this.change, required this.onDispose}) {} final String? paymentId; @@ -130,6 +135,7 @@ class ConfirmSendingAlertContent extends StatefulWidget { final String feeValue; final String feeFiatAmount; final List outputs; + final PendingChange? change; final Function? onDispose; @override @@ -145,6 +151,7 @@ class ConfirmSendingAlertContent extends StatefulWidget { feeValue: feeValue, feeFiatAmount: feeFiatAmount, outputs: outputs, + change: change, onDispose: onDispose); } @@ -161,6 +168,7 @@ class ConfirmSendingAlertContentState extends State required this.feeValue, required this.feeFiatAmount, required this.outputs, + this.change, this.onDispose}) : recipientTitle = '' { recipientTitle = outputs.length > 1 @@ -179,6 +187,7 @@ class ConfirmSendingAlertContentState extends State final String feeValue; final String feeFiatAmount; final List outputs; + final PendingChange? change; final Function? onDispose; final double backgroundHeight = 160; @@ -391,100 +400,57 @@ class ConfirmSendingAlertContentState extends State decoration: TextDecoration.none, ), ), - outputs.length > 1 - ? ListView.builder( - padding: EdgeInsets.only(top: 0), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: outputs.length, - itemBuilder: (context, index) { - final item = outputs[index]; - final _address = - item.isParsedAddress ? item.extractedAddress : item.address; - final _amount = item.cryptoAmount.replaceAll(',', '.'); + ListView.builder( + padding: EdgeInsets.only(top: 0), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: outputs.length, + itemBuilder: (context, index) { + final item = outputs[index]; + final _address = + item.isParsedAddress ? item.extractedAddress : item.address; + final _amount = item.cryptoAmount.replaceAll(',', '.'); - return Column( - children: [ - if (item.isParsedAddress) - Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - item.parsedAddress.name, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - )), - Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - _address, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - )), - Padding( - padding: EdgeInsets.only(top: 8), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - _amount, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - ) - ], - )) - ], - ); - }) - : Column(children: [ - if (outputs.first.isParsedAddress) - Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - outputs.first.parsedAddress.name, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - )), - Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - outputs.first.isParsedAddress - ? outputs.first.extractedAddress - : outputs.first.address, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - )), - ]) + return Column( + children: [ + if (item.isParsedAddress) + AddressText(text: item.parsedAddress.name), + AddressText(text: _address, fontSize: 10), + if (stealthAddressText(item.stealthAddress) != null) + AddressText( + text: stealthAddressText(item.stealthAddress)!, fontSize: 10), + AmountText(text: _amount), + ], + ); + }, + ) ], ), - ) + ), + if (change != null) + Padding( + padding: EdgeInsets.only(top: 16), + child: Column( + children: [ + Text( + S.of(context).send_change_to_you, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + Column( + children: [ + AddressText(text: change!.address, fontSize: 10), + AmountText(text: change!.amount), + ], + ) + ], + ), + ) ], ))), if (showScrollbar) @@ -539,3 +505,78 @@ class ExpirationTimeWidget extends StatelessWidget { ); } } + +class AddressText extends StatelessWidget { + final String text; + final double fontSize; + final FontWeight fontWeight; + final TextAlign? textAlign; + + const AddressText({ + required this.text, + this.fontSize = 14, + this.fontWeight = FontWeight.w600, + this.textAlign, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + text, + style: TextStyle( + fontSize: fontSize, + fontWeight: fontWeight, + fontFamily: 'Lato', + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none, + ), + ), + ); + } +} + +class AmountText extends StatelessWidget { + final String text; + final double fontSize; + final FontWeight fontWeight; + final TextAlign? textAlign; + + const AmountText({ + required this.text, + this.fontSize = 10, + this.fontWeight = FontWeight.w600, + this.textAlign, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + text, + style: TextStyle( + fontSize: fontSize, + fontWeight: fontWeight, + fontFamily: 'Lato', + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none, + ), + ) + ], + )); + } +} + +String? stealthAddressText(String? stealthAddress) { + if (stealthAddress == null) { + return null; + } + + return stealthAddress.isNotEmpty ? "-> $stealthAddress" : null; +} diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 7db244460..53e7686e8 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -58,8 +58,8 @@ class PrivacyPage extends BasePage { if (_privacySettingsViewModel.isAutoGenerateSubaddressesVisible) SettingsSwitcherCell( title: _privacySettingsViewModel.isMoneroWallet - ? S.current.auto_generate_subaddresses - : S.current.auto_generate_addresses, + ? S.current.auto_generate_subaddresses + : S.current.auto_generate_addresses, value: _privacySettingsViewModel.isAutoGenerateSubaddressesEnabled, onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setAutoGenerateSubaddresses(value); @@ -115,21 +115,21 @@ class PrivacyPage extends BasePage { ), if (_privacySettingsViewModel.canUseMempoolFeeAPI) SettingsSwitcherCell( - title: S.current.live_fee_rates, + title: S.current.enable_mempool_api, value: _privacySettingsViewModel.useMempoolFeeAPI, onValueChange: (BuildContext _, bool isEnabled) async { if (!isEnabled) { final bool confirmation = await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).warning, - alertContent: S.of(context).disable_fee_api_warning, - rightButtonText: S.of(context).confirm, - leftButtonText: S.of(context).cancel, - actionRightButton: () => Navigator.of(context).pop(true), - actionLeftButton: () => Navigator.of(context).pop(false)); - }) ?? + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).warning, + alertContent: S.of(context).disable_fee_api_warning, + rightButtonText: S.of(context).confirm, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? false; if (confirmation) { _privacySettingsViewModel.setUseMempoolFeeAPI(isEnabled); diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index e0f83a4f4..40e191438 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -20,6 +20,7 @@ class BlockchainHeightWidget extends StatefulWidget { this.isSilentPaymentsScan = false, this.toggleSingleScan, this.doSingleScan = false, + this.bitcoinMempoolAPIEnabled, required this.walletType, }) : super(key: key); @@ -29,6 +30,7 @@ class BlockchainHeightWidget extends StatefulWidget { final bool hasDatePicker; final bool isSilentPaymentsScan; final bool doSingleScan; + final Future? bitcoinMempoolAPIEnabled; final Function()? toggleSingleScan; final WalletType walletType; @@ -79,7 +81,8 @@ class BlockchainHeightState extends State { child: BaseTextFormField( focusNode: widget.focusNode, controller: restoreHeightController, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false), + keyboardType: + TextInputType.numberWithOptions(signed: false, decimal: false), hintText: widget.isSilentPaymentsScan ? S.of(context).silent_payments_scan_from_height : S.of(context).widgets_restore_from_blockheight, @@ -146,7 +149,9 @@ class BlockchainHeightState extends State { : S.of(context).restore_from_date_or_blockheight, textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, fontWeight: FontWeight.normal, color: Theme.of(context).hintColor), + fontSize: 12, + fontWeight: FontWeight.normal, + color: Theme.of(context).hintColor), ), ) ] @@ -166,7 +171,10 @@ class BlockchainHeightState extends State { if (date != null) { int height; if (widget.isSilentPaymentsScan) { - height = bitcoin!.getHeightByDate(date: date); + height = await bitcoin!.getHeightByDate( + date: date, + bitcoinMempoolAPIEnabled: await widget.bitcoinMempoolAPIEnabled, + ); } else { if (widget.walletType == WalletType.monero) { height = monero!.getHeightByDate(date: date); diff --git a/lib/store/dashboard/transaction_filter_store.dart b/lib/store/dashboard/transaction_filter_store.dart index af0dcce87..fb9ee14cd 100644 --- a/lib/store/dashboard/transaction_filter_store.dart +++ b/lib/store/dashboard/transaction_filter_store.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:mobx/mobx.dart'; @@ -6,12 +7,13 @@ import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; part 'transaction_filter_store.g.dart'; -class TransactionFilterStore = TransactionFilterStoreBase - with _$TransactionFilterStore; +class TransactionFilterStore = TransactionFilterStoreBase with _$TransactionFilterStore; abstract class TransactionFilterStoreBase with Store { - TransactionFilterStoreBase() : displayIncoming = true, - displayOutgoing = true; + TransactionFilterStoreBase() + : displayIncoming = true, + displayOutgoing = true, + displaySilentPayments = true; @observable bool displayIncoming; @@ -19,6 +21,9 @@ abstract class TransactionFilterStoreBase with Store { @observable bool displayOutgoing; + @observable + bool displaySilentPayments; + @observable DateTime? startDate; @@ -26,31 +31,36 @@ abstract class TransactionFilterStoreBase with Store { DateTime? endDate; @computed - bool get displayAll => displayIncoming && displayOutgoing; + bool get displayAll => displayIncoming && displayOutgoing && displaySilentPayments; @action void toggleAll() { if (displayAll) { displayOutgoing = false; displayIncoming = false; + displaySilentPayments = false; } else { displayOutgoing = true; displayIncoming = true; + displaySilentPayments = true; } } - @action void toggleIncoming() { displayIncoming = !displayIncoming; } - @action void toggleOutgoing() { displayOutgoing = !displayOutgoing; } + @action + void toggleSilentPayments() { + displaySilentPayments = !displaySilentPayments; + } + @action void changeStartDate(DateTime date) => startDate = date; @@ -59,34 +69,33 @@ abstract class TransactionFilterStoreBase with Store { List filtered({required List transactions}) { var _transactions = []; - final needToFilter = !displayAll || - (startDate != null && endDate != null); + final needToFilter = !displayAll || (startDate != null && endDate != null); if (needToFilter) { _transactions = transactions.where((item) { var allowed = true; if (allowed && startDate != null && endDate != null) { - if(item is TransactionListItem){ - allowed = (startDate?.isBefore(item.transaction.date) ?? false) - && (endDate?.isAfter(item.transaction.date) ?? false); - }else if(item is AnonpayTransactionListItem){ - allowed = (startDate?.isBefore(item.transaction.createdAt) ?? false) - && (endDate?.isAfter(item.transaction.createdAt) ?? false); - } + if (item is TransactionListItem) { + allowed = (startDate?.isBefore(item.transaction.date) ?? false) && + (endDate?.isAfter(item.transaction.date) ?? false); + } else if (item is AnonpayTransactionListItem) { + allowed = (startDate?.isBefore(item.transaction.createdAt) ?? false) && + (endDate?.isAfter(item.transaction.createdAt) ?? false); + } } if (allowed && (!displayAll)) { - if(item is TransactionListItem){ - allowed = (displayOutgoing && - item.transaction.direction == - TransactionDirection.outgoing) || - (displayIncoming && - item.transaction.direction == TransactionDirection.incoming); - } else if(item is AnonpayTransactionListItem){ + if (item is TransactionListItem) { + allowed = + (displayOutgoing && item.transaction.direction == TransactionDirection.outgoing) || + (displayIncoming && + item.transaction.direction == TransactionDirection.incoming && + !bitcoin!.txIsReceivedSilentPayment(item.transaction)) || + (displaySilentPayments && bitcoin!.txIsReceivedSilentPayment(item.transaction)); + } else if (item is AnonpayTransactionListItem) { allowed = displayIncoming; } - } return allowed; @@ -97,4 +106,4 @@ abstract class TransactionFilterStoreBase with Store { return _transactions; } -} \ No newline at end of file +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index d69d662e1..9aeb3a46b 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -88,6 +88,11 @@ abstract class DashboardViewModelBase with Store { value: () => transactionFilterStore.displayOutgoing, caption: S.current.outgoing, onChanged: transactionFilterStore.toggleOutgoing), + FilterItem( + value: () => transactionFilterStore.displaySilentPayments, + caption: S.current.silent_payments, + onChanged: transactionFilterStore.toggleSilentPayments, + ), // FilterItem( // value: () => false, // caption: S.current.transactions_by_date, @@ -376,12 +381,15 @@ abstract class DashboardViewModelBase with Store { // to not cause work duplication, this will do the job as well, it will be slightly less precise // about what happened - but still enough. // if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0", - if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("")) "private view key is 0", + if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("")) + "private view key is 0", // if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0", - if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) "public view key is 0", + if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) + "public view key is 0", // if (wallet.seed == null) "wallet seed is null", // if (wallet.seed == "") "wallet seed is empty", - if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address == "41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4") + if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address == + "41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4") "primary address is invalid, you won't be able to receive / spend funds", ]; return errors; diff --git a/lib/view_model/rescan_view_model.dart b/lib/view_model/rescan_view_model.dart index dcc81c0a0..00eed5633 100644 --- a/lib/view_model/rescan_view_model.dart +++ b/lib/view_model/rescan_view_model.dart @@ -29,6 +29,10 @@ abstract class RescanViewModelBase with Store { @computed bool get isSilentPaymentsScan => wallet.type == WalletType.bitcoin; + @computed + Future get isBitcoinMempoolAPIEnabled async => + wallet.type == WalletType.bitcoin && await bitcoin!.checkIfMempoolAPIIsEnabled(wallet); + @action Future rescanCurrentWallet({required int restoreHeight}) async { state = RescanWalletState.rescaning; diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 94854df31..e53127e0c 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -79,6 +79,9 @@ abstract class OutputBase with Store { bool get isParsedAddress => parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty; + @observable + String? stealthAddress; + @computed int get formattedCryptoAmount { int amount = 0; @@ -134,9 +137,8 @@ abstract class OutputBase with Store { final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? 0; return double.parse(trc20EstimatedFee.toString()); } - } - + if (_wallet.type == WalletType.solana) { return solana!.getEstimateFees(_wallet) ?? 0.0; } @@ -145,16 +147,16 @@ abstract class OutputBase with Store { _settingsStore.priority[_wallet.type]!, formattedCryptoAmount); if (_wallet.type == WalletType.bitcoin) { - if (_settingsStore.priority[_wallet.type] == bitcoin!.getBitcoinTransactionPriorityCustom()) { - fee = bitcoin!.getEstimatedFeeWithFeeRate(_wallet, - _settingsStore.customBitcoinFeeRate,formattedCryptoAmount); + if (_settingsStore.priority[_wallet.type] == + bitcoin!.getBitcoinTransactionPriorityCustom()) { + fee = bitcoin!.getEstimatedFeeWithFeeRate( + _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } - if (_wallet.type == WalletType.litecoin || - _wallet.type == WalletType.bitcoinCash) { + if (_wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash) { return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } @@ -249,7 +251,8 @@ abstract class OutputBase with Store { try { final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!, - cryptoAmount: sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.')); + cryptoAmount: + sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.')); if (fiatAmount != fiat) { fiatAmount = fiat; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 22c083455..e8b00eb7e 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -375,6 +375,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor throw Exception("THORChain does not support Taproot addresses"); } } + + if (wallet.type == WalletType.bitcoin) { + final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs); + + if (outputs.length == updatedOutputs.length) { + outputs = ObservableList.of(updatedOutputs); + } + } + state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e) { @@ -414,8 +423,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } Future _executeReplaceByFee(TransactionInfo tx, String newFee) async { - - clearOutputs(); final output = outputs.first; output.address = tx.outputAddresses?.first ?? ''; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 0d99c1c8c..54bd54f53 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -100,7 +100,7 @@ dependencies: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v5 + ref: cake-update-v7 ledger_flutter: ^1.0.1 hashlib: ^1.19.2 @@ -138,7 +138,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v5 + ref: cake-update-v7 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index cee29eeb4..4784bb5e3 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -232,8 +232,9 @@ "edit_token": "تحرير الرمز المميز", "electrum_address_disclaimer": "نقوم بإنشاء عناوين جديدة في كل مرة تستخدم فيها عنوانًا ، لكن العناوين السابقة تستمر في العمل", "email_address": "عنوان البريد الالكترونى", + "enable_mempool_api": "MEMPOOL API للحصول على رسوم وتواريخ دقيقة", "enable_replace_by_fee": "تمكين الاستبدال", - "enable_silent_payments_scanning": "تمكين المسح الضوئي للمدفوعات الصامتة", + "enable_silent_payments_scanning": "ابدأ في مسح المدفوعات الصامتة ، حتى يتم الوصول إلى الطرف", "enabled": "ممكنة", "enter_amount": "أدخل المبلغ", "enter_backup_password": "أدخل كلمة المرور الاحتياطية هنا", @@ -295,6 +296,7 @@ "failed_authentication": "${state_error} فشل المصادقة.", "faq": "الأسئلة الشائعة", "features": "سمات", + "fee_rate": "معدل الرسوم", "fetching": "جار الجلب", "fiat_api": "Fiat API", "fiat_balance": "الرصيد فيات", @@ -610,6 +612,7 @@ "send": "إرسال", "send_address": "عنوان ${cryptoCurrency}", "send_amount": "مقدار:", + "send_change_to_you": "تغيير لك:", "send_creating_transaction": " يتم إنشاء المعاملة", "send_error_currency": "العملة يجب أن تحتوي على أرقام فقط", "send_error_minimum_value": "الحد الأدنى لقيمة المبلغ هو 0.01", @@ -676,6 +679,7 @@ "signature_invalid_error": "التوقيع غير صالح للرسالة المقدمة", "signTransaction": " ﺔﻠﻣﺎﻌﻤﻟﺍ ﻊﻴﻗﻮﺗ", "signup_for_card_accept_terms": "قم بالتسجيل للحصول على البطاقة وقبول الشروط.", + "silent_payment": "الدفع الصامت", "silent_payments": "مدفوعات صامتة", "silent_payments_always_scan": "حدد المدفوعات الصامتة دائمًا المسح الضوئي", "silent_payments_disclaimer": "العناوين الجديدة ليست هويات جديدة. إنها إعادة استخدام هوية موجودة مع ملصق مختلف.", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": " (Ethereum، Polygon) ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ EVM ﻊﻣ ﺔﻘﻓﺍﻮﺘﻣ ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ", "symbol": "ﺰﻣﺭ", "sync_all_wallets": "مزامنة جميع المحافظ", + "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": "بدء المسح", + "sync_status_starting_scan": "بدء المسح الضوئي (من ${height})", "sync_status_starting_sync": "بدء المزامنة", "sync_status_syncronized": "متزامن", "sync_status_syncronizing": "يتم المزامنة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 771d58f57..d76e1c763 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -232,8 +232,9 @@ "edit_token": "Редактиране на токена", "electrum_address_disclaimer": "Нови адреси се генерират всеки път, когато използвате този, но и предишните продължават да работят", "email_address": "Имейл адрес", + "enable_mempool_api": "Mempool API за точни такси и дати", "enable_replace_by_fee": "Активиране на замяна по забрана", - "enable_silent_payments_scanning": "Активирайте безшумните плащания за сканиране", + "enable_silent_payments_scanning": "Започнете да сканирате безшумните плащания, докато се достигне съветът", "enabled": "Активирано", "enter_amount": "Въведете сума", "enter_backup_password": "Въведете парола за възстановяване", @@ -295,6 +296,7 @@ "failed_authentication": "Неуспешно удостоверяване. ${state_error}", "faq": "FAQ", "features": "Характеристика", + "fee_rate": "Такса ставка", "fetching": "Обработване", "fiat_api": "Fiat API", "fiat_balance": "Фиат Баланс", @@ -610,6 +612,7 @@ "send": "Изпрати", "send_address": "${cryptoCurrency} адрес", "send_amount": "Сума:", + "send_change_to_you": "Променете, на вас:", "send_creating_transaction": "Създаване на транзакция", "send_error_currency": "Валутата може да съдържа само числа", "send_error_minimum_value": "Минималната сума е 0.01", @@ -676,6 +679,7 @@ "signature_invalid_error": "Подписът не е валиден за даденото съобщение", "signTransaction": "Подпишете транзакция", "signup_for_card_accept_terms": "Регистрайте се за картата и приемете условията.", + "silent_payment": "Безшумно плащане", "silent_payments": "Мълчаливи плащания", "silent_payments_always_scan": "Задайте мълчаливи плащания винаги сканиране", "silent_payments_disclaimer": "Новите адреси не са нови идентичности. Това е повторна употреба на съществуваща идентичност с различен етикет.", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "Моля, превключете към портфейл, съвместим с EVM, и опитайте отново (Ethereum, Polygon)", "symbol": "Символ", "sync_all_wallets": "Синхронизирайте всички портфейли", + "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": "Стартово сканиране", + "sync_status_starting_scan": "Стартиране на сканиране (от ${height})", "sync_status_starting_sync": "ЗАПОЧВАНЕ НА СИНХРОНИЗАЦИЯ", "sync_status_syncronized": "СИНХРОНИЗИРАНО", "sync_status_syncronizing": "СИНХРОНИЗИРАНЕ", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 33039c741..d4c9e1151 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -232,8 +232,9 @@ "edit_token": "Upravit token", "electrum_address_disclaimer": "Po každém použití je generována nová adresa, ale předchozí adresy také stále fungují", "email_address": "E-mailová adresa", + "enable_mempool_api": "Mempool API pro přesné poplatky a data", "enable_replace_by_fee": "Povolit výměnu podle poplatku", - "enable_silent_payments_scanning": "Povolte skenování tichých plateb", + "enable_silent_payments_scanning": "Začněte skenovat tiché platby, dokud není dosaženo špičky", "enabled": "Povoleno", "enter_amount": "Zadejte částku", "enter_backup_password": "Zde zadejte své heslo pro zálohy", @@ -295,6 +296,7 @@ "failed_authentication": "Ověřování selhalo. ${state_error}", "faq": "FAQ", "features": "Funkce", + "fee_rate": "Sazba poplatků", "fetching": "Načítá se", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -610,6 +612,7 @@ "send": "Poslat", "send_address": "${cryptoCurrency} adresa", "send_amount": "Částka:", + "send_change_to_you": "Změňte, vám:", "send_creating_transaction": "Vytváření transakce", "send_error_currency": "Měna může obsahovat pouze čísla", "send_error_minimum_value": "Minimální částka je 0,01", @@ -676,6 +679,7 @@ "signature_invalid_error": "Podpis není platný pro danou zprávu", "signTransaction": "Podepsat transakci", "signup_for_card_accept_terms": "Zaregistrujte se pro kartu a souhlaste s podmínkami.", + "silent_payment": "Tichá platba", "silent_payments": "Tiché platby", "silent_payments_always_scan": "Nastavit tiché platby vždy skenování", "silent_payments_disclaimer": "Nové adresy nejsou nové identity. Je to opětovné použití existující identity s jiným štítkem.", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "Přepněte na peněženku kompatibilní s EVM a zkuste to znovu (Ethereum, Polygon)", "symbol": "Symbol", "sync_all_wallets": "Synchronizovat všechny peněženky", + "sync_status_attempting_scan": "Pokus o skenování", "sync_status_attempting_sync": "ZAHAJUJI SYNCHR.", "sync_status_connected": "PŘIPOJENO", "sync_status_connecting": "PŘIPOJOVÁNÍ", "sync_status_failed_connect": "ODPOJENO", "sync_status_not_connected": "NEPŘIPOJENO", - "sync_status_starting_scan": "Počáteční skenování", + "sync_status_starting_scan": "Počáteční skenování (z ${height})", "sync_status_starting_sync": "SPOUŠTĚNÍ SYNCHRONIZACE", "sync_status_syncronized": "SYNCHRONIZOVÁNO", "sync_status_syncronizing": "SYNCHRONIZUJI", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 98429ea04..8b316fe97 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -232,8 +232,9 @@ "edit_token": "Token bearbeiten", "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", "email_address": "E-Mail-Adresse", + "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": "Aktivieren Sie stille Zahlungen Scannen", + "enable_silent_payments_scanning": "Scannen Sie stille Zahlungen, bis die Spitze erreicht ist", "enabled": "Ermöglicht", "enter_amount": "Betrag eingeben", "enter_backup_password": "Sicherungskennwort hier eingeben", @@ -295,6 +296,7 @@ "failed_authentication": "Authentifizierung fehlgeschlagen. ${state_error}", "faq": "Häufig gestellte Fragen", "features": "Merkmale", + "fee_rate": "Gebührenpreis", "fetching": "Frage ab", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -611,6 +613,7 @@ "send": "Senden", "send_address": "${cryptoCurrency}-Adresse", "send_amount": "Betrag:", + "send_change_to_you": "Verändere dich zu dir:", "send_creating_transaction": "Erstelle Transaktion", "send_error_currency": "Die Währung darf nur Zahlen enthalten", "send_error_minimum_value": "Der Mindestbetrag ist 0,01", @@ -677,6 +680,7 @@ "signature_invalid_error": "Die Signatur gilt nicht für die angegebene Nachricht", "signTransaction": "Transaktion unterzeichnen", "signup_for_card_accept_terms": "Melden Sie sich für die Karte an und akzeptieren Sie die Bedingungen.", + "silent_payment": "Stille Zahlung", "silent_payments": "Stille Zahlungen", "silent_payments_always_scan": "Setzen Sie stille Zahlungen immer scannen", "silent_payments_disclaimer": "Neue Adressen sind keine neuen Identitäten. Es ist eine Wiederverwendung einer bestehenden Identität mit einem anderen Etikett.", @@ -709,12 +713,13 @@ "switchToEVMCompatibleWallet": "Bitte wechseln Sie zu einem EVM-kompatiblen Wallet und versuchen Sie es erneut (Ethereum, Polygon)", "symbol": "Symbol", "sync_all_wallets": "Alle Wallets synchronisieren", + "sync_status_attempting_scan": "Versuch Scan", "sync_status_attempting_sync": "SYNC VERSUCHEN", "sync_status_connected": "VERBUNDEN", "sync_status_connecting": "VERBINDEN", "sync_status_failed_connect": "GETRENNT", "sync_status_not_connected": "NICHT VERBUNDEN", - "sync_status_starting_scan": "Scan beginnen", + "sync_status_starting_scan": "SCAN starten (von ${height})", "sync_status_starting_sync": "STARTE SYNCHRONISIERUNG", "sync_status_syncronized": "SYNCHRONISIERT", "sync_status_syncronizing": "SYNCHRONISIERE", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 7512c3ca0..6858d9359 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -232,8 +232,9 @@ "edit_token": "Edit token", "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", "email_address": "Email Address", + "enable_mempool_api": "Mempool API for accurate fees and dates", "enable_replace_by_fee": "Enable Replace-By-Fee", - "enable_silent_payments_scanning": "Enable silent payments scanning", + "enable_silent_payments_scanning": "Start scanning silent payments, until the tip is reached", "enabled": "Enabled", "enter_amount": "Enter Amount", "enter_backup_password": "Enter backup password here", @@ -295,6 +296,7 @@ "failed_authentication": "Failed authentication. ${state_error}", "faq": "FAQ", "features": "Features", + "fee_rate": "Fee rate", "fetching": "Fetching", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -611,6 +613,7 @@ "send": "Send", "send_address": "${cryptoCurrency} address", "send_amount": "Amount:", + "send_change_to_you": "Change, to you:", "send_creating_transaction": "Creating transaction", "send_error_currency": "Currency can only contain numbers", "send_error_minimum_value": "Minimum value of amount is 0.01", @@ -677,6 +680,7 @@ "signature_invalid_error": "The signature is not valid for the message given", "signTransaction": "Sign Transaction", "signup_for_card_accept_terms": "Sign up for the card and accept the terms.", + "silent_payment": "Silent Payment", "silent_payments": "Silent Payments", "silent_payments_always_scan": "Set Silent Payments always scanning", "silent_payments_disclaimer": "New addresses are not new identities. It is a re-use of an existing identity with a different label.", @@ -709,12 +713,13 @@ "switchToEVMCompatibleWallet": "Please switch to an EVM compatible wallet and try again (Ethereum, Polygon)", "symbol": "Symbol", "sync_all_wallets": "Sync all wallets", + "sync_status_attempting_scan": "ATTEMPTING SCAN", "sync_status_attempting_sync": "ATTEMPTING SYNC", "sync_status_connected": "CONNECTED", "sync_status_connecting": "CONNECTING", "sync_status_failed_connect": "DISCONNECTED", "sync_status_not_connected": "NOT CONNECTED", - "sync_status_starting_scan": "STARTING SCAN", + "sync_status_starting_scan": "STARTING SCAN (from ${height})", "sync_status_starting_sync": "STARTING SYNC", "sync_status_syncronized": "SYNCHRONIZED", "sync_status_syncronizing": "SYNCHRONIZING", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 66e17f1d5..ab92499f7 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -232,8 +232,9 @@ "edit_token": "Editar token", "electrum_address_disclaimer": "Generamos nuevas direcciones cada vez que usa una, pero las direcciones anteriores siguen funcionando", "email_address": "Dirección de correo electrónico", + "enable_mempool_api": "API de Mempool para tarifas y fechas precisas", "enable_replace_by_fee": "Habilitar reemplazar por tarea", - "enable_silent_payments_scanning": "Habilitar escaneo de pagos silenciosos", + "enable_silent_payments_scanning": "Comience a escanear pagos silenciosos, hasta que se alcance la punta", "enabled": "Activado", "enter_amount": "Ingrese la cantidad", "enter_backup_password": "Ingrese la contraseña de respaldo aquí", @@ -295,6 +296,7 @@ "failed_authentication": "Autenticación fallida. ${state_error}", "faq": "FAQ", "features": "Características", + "fee_rate": "Tarifa", "fetching": "Cargando", "fiat_api": "Fiat API", "fiat_balance": "Equilibrio Fiat", @@ -611,6 +613,7 @@ "send": "Enviar", "send_address": "Dirección de ${cryptoCurrency}", "send_amount": "Cantidad:", + "send_change_to_you": "Cambiar, a ti:", "send_creating_transaction": "Creando transacción", "send_error_currency": "La moneda solo puede contener números", "send_error_minimum_value": "El valor mínimo de la cantidad es 0.01", @@ -677,6 +680,7 @@ "signature_invalid_error": "La firma no es válida para el mensaje dado", "signTransaction": "Firmar transacción", "signup_for_card_accept_terms": "Regístrese para obtener la tarjeta y acepte los términos.", + "silent_payment": "Pago silencioso", "silent_payments": "Pagos silenciosos", "silent_payments_always_scan": "Establecer pagos silenciosos siempre escaneando", "silent_payments_disclaimer": "Las nuevas direcciones no son nuevas identidades. Es una reutilización de una identidad existente con una etiqueta diferente.", @@ -709,12 +713,13 @@ "switchToEVMCompatibleWallet": "Cambie a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)", "symbol": "Símbolo", "sync_all_wallets": "Sincronizar todas las billeteras", + "sync_status_attempting_scan": "Intento de escaneo", "sync_status_attempting_sync": "INTENTAR SINCRONIZAR", "sync_status_connected": "CONECTADO", "sync_status_connecting": "CONECTANDO", "sync_status_failed_connect": "DESCONECTADO", "sync_status_not_connected": "NO CONECTADO", - "sync_status_starting_scan": "Escaneo inicial", + "sync_status_starting_scan": "Iniciar escaneo (de ${height})", "sync_status_starting_sync": "EMPEZANDO A SINCRONIZAR", "sync_status_syncronized": "SINCRONIZADO", "sync_status_syncronizing": "SINCRONIZANDO", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index da73d0c00..d062cdd86 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -232,8 +232,9 @@ "edit_token": "Modifier le token", "electrum_address_disclaimer": "Nous générons de nouvelles adresses à chaque fois que vous en utilisez une, mais les adresses précédentes continuent à fonctionner", "email_address": "Adresse e-mail", + "enable_mempool_api": "API Mempool pour les frais et dates précis", "enable_replace_by_fee": "Activer Remplace-by-Fee", - "enable_silent_payments_scanning": "Activer la numérisation des paiements silencieux", + "enable_silent_payments_scanning": "Commencez à scanner les paiements silencieux, jusqu'à ce que la pointe soit atteinte", "enabled": "Activé", "enter_amount": "Entrez le montant", "enter_backup_password": "Entrez le mot de passe de sauvegarde ici", @@ -295,6 +296,7 @@ "failed_authentication": "Échec d'authentification. ${state_error}", "faq": "FAQ", "features": "Caractéristiques", + "fee_rate": "Taux de frais", "fetching": "Récupération", "fiat_api": "Fiat API", "fiat_balance": "Solde fiat", @@ -610,6 +612,7 @@ "send": "Envoyer", "send_address": "adresse ${cryptoCurrency}", "send_amount": "Montant :", + "send_change_to_you": "Changer, pour vous:", "send_creating_transaction": "Création de la transaction", "send_error_currency": "La monnaie ne peut contenir que des nombres", "send_error_minimum_value": "La valeur minimale du montant est 0.01", @@ -676,6 +679,7 @@ "signature_invalid_error": "La signature n'est pas valable pour le message donné", "signTransaction": "Signer une transaction", "signup_for_card_accept_terms": "Inscrivez-vous pour la carte et acceptez les conditions.", + "silent_payment": "Paiement silencieux", "silent_payments": "Paiements silencieux", "silent_payments_always_scan": "Définir les paiements silencieux toujours à la scanne", "silent_payments_disclaimer": "Les nouvelles adresses ne sont pas de nouvelles identités. Il s'agit d'une réutilisation d'une identité existante avec une étiquette différente.", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "Veuillez passer à un portefeuille compatible EVM et réessayer (Ethereum, Polygon)", "symbol": "Symbole", "sync_all_wallets": "Synchroniser tous les portefeuilles", + "sync_status_attempting_scan": "Tentative de numérisation", "sync_status_attempting_sync": "TENTATIVE DE SYNCHRONISATION", "sync_status_connected": "CONNECTÉ", "sync_status_connecting": "CONNEXION EN COURS", "sync_status_failed_connect": "DÉCONNECTÉ", "sync_status_not_connected": "NON CONNECTÉ", - "sync_status_starting_scan": "Démarrage", + "sync_status_starting_scan": "Démarrer la numérisation (à partir de ${height})", "sync_status_starting_sync": "DÉBUT DE SYNCHRO", "sync_status_syncronized": "SYNCHRONISÉ", "sync_status_syncronizing": "SYNCHRONISATION EN COURS", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 387c1a0b0..017fd7e01 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -232,8 +232,9 @@ "edit_token": "Gyara alamar", "electrum_address_disclaimer": "Muna samar da sababbin adireshi duk lokacin da kuka yi amfani da ɗaya, amma adiresoshin da suka gabata suna ci gaba da aiki", "email_address": "Adireshin i-mel", + "enable_mempool_api": "Mampool API don ingantattun kudade da kwanakin", "enable_replace_by_fee": "Ba da damar maye gurbin-by-kudin", - "enable_silent_payments_scanning": "Kunna biya biya", + "enable_silent_payments_scanning": "Fara bincika biya na shiru, har sai tip ɗin ya kai", "enabled": "An kunna", "enter_amount": "Shigar da Adadi", "enter_backup_password": "Shigar da kalmar wucewa ta madadin nan", @@ -295,6 +296,7 @@ "failed_authentication": "Binne wajen shiga. ${state_error}", "faq": "FAQ", "features": "Fasas", + "fee_rate": "Kudi", "fetching": "Daukewa", "fiat_api": "API ɗin Fiat", "fiat_balance": "Fiat Balance", @@ -612,6 +614,7 @@ "send": "Aika", "send_address": "${cryptoCurrency} address", "send_amount": "Adadi:", + "send_change_to_you": "Canza, a gare ku:", "send_creating_transaction": "Ƙirƙirar ciniki", "send_error_currency": "Kudi zai iya ƙunsar lambobi kawai", "send_error_minimum_value": "Mafi ƙarancin ƙimar adadin shine 0.01", @@ -678,6 +681,7 @@ "signature_invalid_error": "Sa hannu ba shi da inganci ga sakon da aka bayar", "signTransaction": "Sa hannu Ma'amala", "signup_for_card_accept_terms": "Yi rajista don katin kuma karɓi sharuɗɗan.", + "silent_payment": "Biya silent", "silent_payments": "Biya silent", "silent_payments_always_scan": "Saita biya na shiru koyaushe", "silent_payments_disclaimer": "Sabbin adiresoshin ba sabon tsari bane. Wannan shine sake amfani da asalin asalin tare da wata alama daban.", @@ -710,12 +714,13 @@ "switchToEVMCompatibleWallet": "Da fatan za a canza zuwa walat ɗin EVM mai jituwa kuma a sake gwadawa (Ethereum, Polygon)", "symbol": "Alama", "sync_all_wallets": "Sync Duk Wallet", + "sync_status_attempting_scan": "Yunƙurin scan", "sync_status_attempting_sync": "KWAFI", "sync_status_connected": "HANNU", "sync_status_connecting": "HADA", "sync_status_failed_connect": "BABU INTERNET", "sync_status_not_connected": "BABU INTERNET", - "sync_status_starting_scan": "Fara scan", + "sync_status_starting_scan": "Farawa Scan (daga ${height})", "sync_status_starting_sync": "KWAFI", "sync_status_syncronized": "KYAU", "sync_status_syncronizing": "KWAFI", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index a0d826c98..95eb228f4 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -232,8 +232,9 @@ "edit_token": "टोकन संपादित करें", "electrum_address_disclaimer": "हर बार जब आप एक का उपयोग करते हैं तो हम नए पते उत्पन्न करते हैं, लेकिन पिछले पते काम करना जारी रखते हैं", "email_address": "ईमेल पता", + "enable_mempool_api": "सटीक शुल्क और तिथियों के लिए मेमपूल एपीआई", "enable_replace_by_fee": "प्रतिस्थापित-दर-शुल्क सक्षम करें", - "enable_silent_payments_scanning": "मूक भुगतान स्कैनिंग सक्षम करें", + "enable_silent_payments_scanning": "साइलेंट पेमेंट्स को स्कैन करना शुरू करें, जब तक कि टिप तक पहुंच न जाए", "enabled": "सक्रिय", "enter_amount": "राशि दर्ज करें", "enter_backup_password": "यहां बैकअप पासवर्ड डालें", @@ -295,6 +296,7 @@ "failed_authentication": "प्रमाणीकरण विफल. ${state_error}", "faq": "FAQ", "features": "विशेषताएँ", + "fee_rate": "शुल्क दर", "fetching": "ला रहा है", "fiat_api": "फिएट पैसे API", "fiat_balance": "फिएट बैलेंस", @@ -612,6 +614,7 @@ "send": "संदेश", "send_address": "${cryptoCurrency} पता", "send_amount": "रकम:", + "send_change_to_you": "बदलो, आप को:", "send_creating_transaction": "लेन-देन बनाना", "send_error_currency": "मुद्रा में केवल संख्याएँ हो सकती हैं", "send_error_minimum_value": "राशि का न्यूनतम मूल्य 0.01 है", @@ -678,6 +681,7 @@ "signature_invalid_error": "हस्ताक्षर दिए गए संदेश के लिए मान्य नहीं है", "signTransaction": "लेन-देन पर हस्ताक्षर करें", "signup_for_card_accept_terms": "कार्ड के लिए साइन अप करें और शर्तें स्वीकार करें।", + "silent_payment": "मूक भुगतान", "silent_payments": "मूक भुगतान", "silent_payments_always_scan": "मूक भुगतान हमेशा स्कैनिंग सेट करें", "silent_payments_disclaimer": "नए पते नई पहचान नहीं हैं। यह एक अलग लेबल के साथ एक मौजूदा पहचान का पुन: उपयोग है।", @@ -710,12 +714,13 @@ "switchToEVMCompatibleWallet": "कृपया ईवीएम संगत वॉलेट पर स्विच करें और पुनः प्रयास करें (एथेरियम, पॉलीगॉन)", "symbol": "प्रतीक", "sync_all_wallets": "सभी वॉलेट सिंक करें", + "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": "स्कैन शुरू करना", + "sync_status_starting_scan": "स्कैन शुरू करना (${height} से)", "sync_status_starting_sync": "सिताज़ा करना", "sync_status_syncronized": "सिंक्रनाइज़", "sync_status_syncronizing": "सिंक्रनाइज़ करने", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 6a7214201..28f1c05b1 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -232,8 +232,9 @@ "edit_token": "Uredi token", "electrum_address_disclaimer": "Minden egyes alkalommal új címeket generálunk, de a korábbi címek továbbra is működnek", "email_address": "Adresa e-pošte", + "enable_mempool_api": "Mempool API za točne naknade i datume", "enable_replace_by_fee": "Omogući zamjenu", - "enable_silent_payments_scanning": "Omogući skeniranje tihih plaćanja", + "enable_silent_payments_scanning": "Započnite skeniranje tihih plaćanja, dok se ne postigne savjet", "enabled": "Omogućeno", "enter_amount": "Unesite iznos", "enter_backup_password": "Unesite svoju lozinku za sigurnosnu kopiju ovdje", @@ -295,6 +296,7 @@ "failed_authentication": "Autentifikacija neuspješna. ${state_error}", "faq": "FAQ", "features": "Značajke", + "fee_rate": "Stopa naknade", "fetching": "Dohvaćanje", "fiat_api": "Fiat API", "fiat_balance": "Fiat Bilans", @@ -610,6 +612,7 @@ "send": "Pošalji", "send_address": "${cryptoCurrency} adresa", "send_amount": "Iznos:", + "send_change_to_you": "Promijenite, u vas:", "send_creating_transaction": "Izrada transakcije", "send_error_currency": "Iznos smije sadržavati samo brojeve", "send_error_minimum_value": "Minimalna vrijednost iznosa je 0.01", @@ -676,6 +679,7 @@ "signature_invalid_error": "Potpis ne vrijedi za danu poruku", "signTransaction": "Potpišite transakciju", "signup_for_card_accept_terms": "Prijavite se za karticu i prihvatite uvjete.", + "silent_payment": "Tiho plaćanje", "silent_payments": "Tiha plaćanja", "silent_payments_always_scan": "Postavite tiho plaćanje uvijek skeniranje", "silent_payments_disclaimer": "Nove adrese nisu novi identiteti. To je ponovna upotreba postojećeg identiteta s drugom oznakom.", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "Prijeđite na novčanik kompatibilan s EVM-om i pokušajte ponovno (Ethereum, Polygon)", "symbol": "Simbol", "sync_all_wallets": "Sinkronizirajte sve novčanike", + "sync_status_attempting_scan": "Pokušaj skeniranja", "sync_status_attempting_sync": "POKUŠAJ SINKRONIZACIJE", "sync_status_connected": "SPOJENO", "sync_status_connecting": "SPAJANJE", "sync_status_failed_connect": "ISKLJUČENO", "sync_status_not_connected": "NIJE POVEZANO", - "sync_status_starting_scan": "Početno skeniranje", + "sync_status_starting_scan": "Početno skeniranje (od ${height})", "sync_status_starting_sync": "ZAPOČINJEMO SINKRONIZIRANJE", "sync_status_syncronized": "SINKRONIZIRANO", "sync_status_syncronizing": "SINKRONIZIRANJE", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 21b940a63..56d45deb1 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -231,8 +231,9 @@ "edit_token": "Փոփոխել տոկեն", "electrum_address_disclaimer": "Մենք ստեղծում ենք նոր հասցե ամեն անգամ, երբ դուք օգտագործում եք այն, բայց նախորդ հասցեները շարունակում են աշխատել", "email_address": "Էլ. փոստի հասցե", + "enable_mempool_api": "Mempool API ճշգրիտ վճարների եւ ամսաթվերի համար", "enable_replace_by_fee": "Միացնել փոխարինումը միջնորդավճարով", - "enable_silent_payments_scanning": "Միացնել Լուռ Վճարումների սկանավորումը", + "enable_silent_payments_scanning": "Սկսեք սկանավորել լուռ վճարումները, մինչեւ որ ծայրը հասնի", "enabled": "Միացված", "enter_amount": "Մուտքագրեք գումար", "enter_backup_password": "Մուտքագրեք կրկնօրինակի գաղտնաբառը", @@ -294,6 +295,7 @@ "failed_authentication": "Վավերացումը ձախողվեց. ${state_error}", "faq": "Հաճախ տրվող հարցեր", "features": "Հատկանիշներ", + "fee_rate": "Վճարման տոկոսադրույքը", "fetching": "Ստացվում է", "fiat_api": "Fiat API", "fiat_balance": "Fiat մնացորդ", @@ -610,6 +612,7 @@ "send": "Ուղարկել", "send_address": "${cryptoCurrency} հասցե", "send_amount": "Քանակ՝", + "send_change_to_you": "Փոփոխություն, ձեզ համար.", "send_creating_transaction": "Ստեղծել գործարք", "send_error_currency": "Արժույթը կարող է պարունակել միայն թվեր", "send_error_minimum_value": "Քանակի նվազագույն արժեքը 0.01 է", @@ -676,6 +679,7 @@ "signature_invalid_error": "Կնիքը անվավեր է տրված հաղորդագրության համար", "signTransaction": "Կնքել Գործարք", "signup_for_card_accept_terms": "Գրանցվել քարտի համար և ընդունել պայմանները", + "silent_payment": "Լուռ վճարում", "silent_payments": "Լուռ Վճարումներ", "silent_payments_always_scan": "Միացնել Լուռ Վճարումներ մշտական սկանավորումը", "silent_payments_disclaimer": "Նոր հասցեները նոր ինքնություն չեն։ Դա այլ պիտակով գոյություն ունեցող ինքնության վերագործածում է", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "Խնդրում ենք անցնել EVM համատեղելի դրամապանակ և փորձել կրկին (Ethereum, Polygon)", "symbol": "Նշան", "sync_all_wallets": "Համաժամեցնել բոլոր դրամապանակները", + "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": "ՍԿԱՆԱՎՈՐՈՒՄԸ ՍԿՍՎՈՒՄ Է", + "sync_status_starting_scan": "Սկսած սկան (${height})", "sync_status_starting_sync": "ՀԱՄԱԺԱՄԵՑՈՒՄԸ ՍԿՍՎՈՒՄ Է", "sync_status_syncronized": "ՀԱՄԱԺԱՄԵՑՎԱԾԷ", "sync_status_syncronizing": "ՀԱՄԱԺԱՄԵՑՎՈՒՄ Է", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index f677a65a6..77a2c68e2 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -232,8 +232,9 @@ "edit_token": "Mengedit token", "electrum_address_disclaimer": "Kami menghasilkan alamat baru setiap kali Anda menggunakan satu, tetapi alamat sebelumnya tetap berfungsi", "email_address": "Alamat Email", + "enable_mempool_api": "API Mempool untuk biaya dan tanggal yang akurat", "enable_replace_by_fee": "Aktifkan ganti-by-fee", - "enable_silent_payments_scanning": "Aktifkan pemindaian pembayaran diam", + "enable_silent_payments_scanning": "Mulailah memindai pembayaran diam, sampai ujung tercapai", "enabled": "Diaktifkan", "enter_amount": "Masukkan Jumlah", "enter_backup_password": "Masukkan kata sandi cadangan di sini", @@ -295,6 +296,7 @@ "failed_authentication": "Otentikasi gagal. ${state_error}", "faq": "Pertanyaan yang Sering Diajukan", "features": "Fitur", + "fee_rate": "Tarif biaya", "fetching": "Mengambil", "fiat_api": "API fiat", "fiat_balance": "Saldo Fiat", @@ -613,6 +615,7 @@ "send": "Mengirim", "send_address": "Alamat ${cryptoCurrency}", "send_amount": "Jumlah:", + "send_change_to_you": "Ubah, untukmu:", "send_creating_transaction": "Membuat transaksi", "send_error_currency": "Mata uang hanya dapat berisi angka", "send_error_minimum_value": "Nilai minimum jumlah adalah 0.01", @@ -679,6 +682,7 @@ "signature_invalid_error": "Tanda tangan tidak valid untuk pesan yang diberikan", "signTransaction": "Tandatangani Transaksi", "signup_for_card_accept_terms": "Daftar untuk kartu dan terima syarat dan ketentuan.", + "silent_payment": "Pembayaran diam", "silent_payments": "Pembayaran diam", "silent_payments_always_scan": "Tetapkan pembayaran diam selalu pemindaian", "silent_payments_disclaimer": "Alamat baru bukanlah identitas baru. Ini adalah penggunaan kembali identitas yang ada dengan label yang berbeda.", @@ -711,12 +715,13 @@ "switchToEVMCompatibleWallet": "Silakan beralih ke dompet yang kompatibel dengan EVM dan coba lagi (Ethereum, Polygon)", "symbol": "Simbol", "sync_all_wallets": "Sinkronkan semua dompet", + "sync_status_attempting_scan": "Mencoba memindai", "sync_status_attempting_sync": "MENCOBA SINKRONISASI", "sync_status_connected": "TERHUBUNG", "sync_status_connecting": "MENGHUBUNGKAN", "sync_status_failed_connect": "GAGAL TERHUBUNG", "sync_status_not_connected": "TIDAK TERHUBUNG", - "sync_status_starting_scan": "Mulai pindai", + "sync_status_starting_scan": "Mulai pemindaian (dari ${height})", "sync_status_starting_sync": "MULAI SINKRONISASI", "sync_status_syncronized": "SUDAH TERSINKRONISASI", "sync_status_syncronizing": "SEDANG SINKRONISASI", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 94cd0fa1d..161f46ad9 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -233,8 +233,9 @@ "edit_token": "Modifica token", "electrum_address_disclaimer": "Generiamo nuovi indirizzi ogni volta che ne utilizzi uno, ma gli indirizzi precedenti continuano a funzionare", "email_address": "Indirizzo e-mail", + "enable_mempool_api": "API di Mempool per commissioni e date accurate", "enable_replace_by_fee": "Abilita sostituzione per fee", - "enable_silent_payments_scanning": "Abilita la scansione dei pagamenti silenziosi", + "enable_silent_payments_scanning": "Inizia a scansionare i pagamenti silenziosi, fino a raggiungere la punta", "enabled": "Abilitato", "enter_amount": "Inserisci importo", "enter_backup_password": "Inserisci la password di backup qui", @@ -296,6 +297,7 @@ "failed_authentication": "Autenticazione fallita. ${state_error}", "faq": "Domande Frequenti", "features": "Caratteristiche", + "fee_rate": "Tasso di commissione", "fetching": "Recupero", "fiat_api": "Fiat API", "fiat_balance": "Equilibrio fiat", @@ -612,6 +614,7 @@ "send": "Invia", "send_address": "${cryptoCurrency} indirizzo", "send_amount": "Ammontare:", + "send_change_to_you": "Cambiamento, a te:", "send_creating_transaction": "Creazione della transazione", "send_error_currency": "L'ammontare può contenere solo numeri", "send_error_minimum_value": "L'ammontare minimo è 0.01", @@ -678,6 +681,7 @@ "signature_invalid_error": "La firma non è valida per il messaggio dato", "signTransaction": "Firma la transazione", "signup_for_card_accept_terms": "Registrati per la carta e accetta i termini.", + "silent_payment": "Pagamento silenzioso", "silent_payments": "Pagamenti silenziosi", "silent_payments_always_scan": "Impostare i pagamenti silenziosi che scansionano sempre", "silent_payments_disclaimer": "I nuovi indirizzi non sono nuove identità. È un riutilizzo di un'identità esistente con un'etichetta diversa.", @@ -710,12 +714,13 @@ "switchToEVMCompatibleWallet": "Passa a un portafoglio compatibile con EVM e riprova (Ethereum, Polygon)", "symbol": "Simbolo", "sync_all_wallets": "Sincronizza tutti i portafogli", + "sync_status_attempting_scan": "Tentando la scansione", "sync_status_attempting_sync": "TENTATIVO DI SINCRONIZZAZIONE", "sync_status_connected": "CONNESSO", "sync_status_connecting": "CONNESSIONE", "sync_status_failed_connect": "DISCONNESSO", "sync_status_not_connected": "NON CONNESSO", - "sync_status_starting_scan": "Scansione di partenza", + "sync_status_starting_scan": "Avvia scansione (da ${height})", "sync_status_starting_sync": "INIZIO SINC", "sync_status_syncronized": "SINCRONIZZATO", "sync_status_syncronizing": "SINCRONIZZAZIONE", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 2dd24346c..8c465f2f7 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -232,8 +232,9 @@ "edit_token": "トークンの編集", "electrum_address_disclaimer": "使用するたびに新しいアドレスが生成されますが、以前のアドレスは引き続き機能します", "email_address": "メールアドレス", + "enable_mempool_api": "正確な料金と日付のMempool API", "enable_replace_by_fee": "交換ごとに有効にします", - "enable_silent_payments_scanning": "サイレントペイメントスキャンを有効にします", + "enable_silent_payments_scanning": "先端に達するまで、サイレント決済のスキャンを開始します", "enabled": "有効", "enter_amount": "金額を入力", "enter_backup_password": "ここにバックアップパスワードを入力してください", @@ -295,6 +296,7 @@ "failed_authentication": "認証失敗. ${state_error}", "faq": "FAQ", "features": "特徴", + "fee_rate": "料金金利", "fetching": "フェッチング", "fiat_api": "不換紙幣 API", "fiat_balance": "フィアットバランス", @@ -611,6 +613,7 @@ "send": "送る", "send_address": "${cryptoCurrency} 住所", "send_amount": "量:", + "send_change_to_you": "あなたに変更:", "send_creating_transaction": "トランザクションを作成する", "send_error_currency": "通貨には数字のみを含めることができます", "send_error_minimum_value": "金額の最小値は0.01です", @@ -677,6 +680,7 @@ "signature_invalid_error": "署名は、指定されたメッセージに対して無効です", "signTransaction": "トランザクションに署名する", "signup_for_card_accept_terms": "カードにサインアップして、利用規約に同意してください。", + "silent_payment": "サイレント支払い", "silent_payments": "サイレント支払い", "silent_payments_always_scan": "サイレント決済を常にスキャンします", "silent_payments_disclaimer": "新しいアドレスは新しいアイデンティティではありません。これは、異なるラベルを持つ既存のアイデンティティの再利用です。", @@ -709,12 +713,13 @@ "switchToEVMCompatibleWallet": "EVM 互換のウォレットに切り替えて再試行してください (イーサリアム、ポリゴン)", "symbol": "シンボル", "sync_all_wallets": "すべてのウォレットを同期", + "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": "スキャンを開始します", + "sync_status_starting_scan": "スキャンを開始する(${height} から)", "sync_status_starting_sync": "同期の開始", "sync_status_syncronized": "同期された", "sync_status_syncronizing": "同期", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 9780de120..a5325ddff 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -232,8 +232,9 @@ "edit_token": "토큰 편집", "electrum_address_disclaimer": "사용할 때마다 새 주소가 생성되지만 이전 주소는 계속 작동합니다.", "email_address": "이메일 주소", + "enable_mempool_api": "정확한 수수료 및 날짜에 대한 Mempool API", "enable_replace_by_fee": "대체별로 활성화하십시오", - "enable_silent_payments_scanning": "무음 지불 스캔을 활성화합니다", + "enable_silent_payments_scanning": "팁에 도달 할 때까지 사일런트 지불을 스캔하기 시작합니다.", "enabled": "사용", "enter_amount": "금액 입력", "enter_backup_password": "여기에 백업 비밀번호를 입력하세요.", @@ -295,6 +296,7 @@ "failed_authentication": "인증 실패. ${state_error}", "faq": "FAQ", "features": "특징", + "fee_rate": "수수료", "fetching": "가져 오는 중", "fiat_api": "명목 화폐 API", "fiat_balance": "피아트 잔액", @@ -611,6 +613,7 @@ "send": "보내다", "send_address": "${cryptoCurrency} 주소", "send_amount": "양:", + "send_change_to_you": "당신에게 변경 :", "send_creating_transaction": "거래 생성", "send_error_currency": "통화는 숫자 만 포함 할 수 있습니다", "send_error_minimum_value": "금액의 최소값은 0.01입니다", @@ -677,6 +680,7 @@ "signature_invalid_error": "서명은 주어진 메시지에 유효하지 않습니다", "signTransaction": "거래 서명", "signup_for_card_accept_terms": "카드에 가입하고 약관에 동의합니다.", + "silent_payment": "조용한 지불", "silent_payments": "조용한 지불", "silent_payments_always_scan": "무음금을 항상 스캔합니다", "silent_payments_disclaimer": "새로운 주소는 새로운 정체성이 아닙니다. 다른 레이블로 기존 신원을 재사용하는 것입니다.", @@ -709,12 +713,13 @@ "switchToEVMCompatibleWallet": "EVM 호환 지갑으로 전환 후 다시 시도해 주세요. (이더리움, 폴리곤)", "symbol": "상징", "sync_all_wallets": "모든 지갑 동기화", + "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": "스캔 시작", + "sync_status_starting_scan": "시작 스캔 (${height} 에서)", "sync_status_starting_sync": "동기화 시작", "sync_status_syncronized": "동기화", "sync_status_syncronizing": "동기화", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index af28f5e9b..d2332092a 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -232,8 +232,9 @@ "edit_token": "တိုကင်ကို တည်းဖြတ်ပါ။", "electrum_address_disclaimer": "သင်အသုံးပြုသည့်အချိန်တိုင်းတွင် ကျွန်ုပ်တို့သည် လိပ်စာအသစ်များကို ထုတ်ပေးသော်လည်း ယခင်လိပ်စာများသည် ဆက်လက်အလုပ်လုပ်နေပါသည်။", "email_address": "အီးမေးလ်လိပ်စာ", + "enable_mempool_api": "Mempool API တိကျသောအခကြေးငွေနှင့်ရက်စွဲများအတွက်", "enable_replace_by_fee": "အစားထိုး - by- အခကြေးငွေ enable", - "enable_silent_payments_scanning": "အသံတိတ်ငွေပေးချေမှုကို scanable လုပ်ပါ", + "enable_silent_payments_scanning": "အစွန်အဖျားသို့ရောက်ရှိသည်အထိအသံတိတ်ငွေပေးချေမှုကိုစကင်ဖတ်စစ်ဆေးပါ", "enabled": "ဖွင့်ထားသည်။", "enter_amount": "ပမာဏကို ထည့်ပါ။", "enter_backup_password": "အရန်စကားဝှက်ကို ဤနေရာတွင် ထည့်ပါ။", @@ -295,6 +296,7 @@ "failed_authentication": "အထောက်အထားစိစစ်ခြင်း မအောင်မြင်ပါ။. ${state_error}", "faq": "အမြဲမေးလေ့ရှိသောမေးခွန်းများ", "features": "အင်္ဂါရပ်များ", + "fee_rate": "ကြေးနှုန်း", "fetching": "ခေါ်ယူခြင်း။", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -610,6 +612,7 @@ "send": "ပို့ပါ။", "send_address": "${cryptoCurrency} လိပ်စာ", "send_amount": "ပမာဏ-", + "send_change_to_you": "ပြောင်းလဲမှု,", "send_creating_transaction": "အရောင်းအဝယ်ပြုလုပ်ခြင်း။", "send_error_currency": "ငွေကြေးတွင် နံပါတ်များသာ ပါဝင်နိုင်သည်။", "send_error_minimum_value": "ပမာဏ၏ အနည်းဆုံးတန်ဖိုးမှာ 0.01 ဖြစ်သည်။", @@ -676,6 +679,7 @@ "signature_invalid_error": "အဆိုပါလက်မှတ်ပေးထားသောမက်ဆေ့ခ်ျကိုများအတွက်မမှန်ကန်ပါ", "signTransaction": "ငွေလွှဲဝင်ပါ။", "signup_for_card_accept_terms": "ကတ်အတွက် စာရင်းသွင်းပြီး စည်းကမ်းချက်များကို လက်ခံပါ။", + "silent_payment": "အသံတိတ်ငွေပေးချေမှု", "silent_payments": "အသံတိတ်ငွေပေးချေမှု", "silent_payments_always_scan": "အမြဲတမ်း scanning အမြဲ scanning", "silent_payments_disclaimer": "လိပ်စာအသစ်များသည်အထောက်အထားအသစ်များမဟုတ်ပါ။ ၎င်းသည်ကွဲပြားခြားနားသောတံဆိပ်ဖြင့်ရှိပြီးသားဝိသေသလက်ခဏာကိုပြန်လည်အသုံးပြုခြင်းဖြစ်သည်။", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "ကျေးဇူးပြု၍ EVM တွဲဖက်သုံးနိုင်သော ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ (Ethereum၊ Polygon)", "symbol": "သင်္ကေတ", "sync_all_wallets": "အားလုံးပိုက်ဆံအိတ်စည်းညှိ", + "sync_status_attempting_scan": "scan ကြိုးစားနေ", "sync_status_attempting_sync": "ချိန်ကိုက်ခြင်းကို ကြိုးစားနေသည်။", "sync_status_connected": "ချိတ်ဆက်ထားသည်။", "sync_status_connecting": "ချိတ်ဆက်ခြင်း။", "sync_status_failed_connect": "အဆက်အသွယ်ဖြတ်ထားသည်။", "sync_status_not_connected": "မချိတ်ဆက်ပါ။", - "sync_status_starting_scan": "စကင်ဖတ်စစ်ဆေးမှု", + "sync_status_starting_scan": "စကင်ဖတ်စစ်ဆေးမှုစတင်ခြင်း (${height})", "sync_status_starting_sync": "စင့်ခ်လုပ်ခြင်း။", "sync_status_syncronized": "ထပ်တူပြုထားသည်။", "sync_status_syncronizing": "ထပ်တူပြုခြင်း။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index c6a107d6d..fcbf0f7c9 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -232,8 +232,9 @@ "edit_token": "Token bewerken", "electrum_address_disclaimer": "We genereren nieuwe adressen elke keer dat u er een gebruikt, maar eerdere adressen blijven werken", "email_address": "E-mailadres", + "enable_mempool_api": "Mempool API voor nauwkeurige kosten en datums", "enable_replace_by_fee": "Schakel vervangen door een fee", - "enable_silent_payments_scanning": "Schakel stille betalingen in scannen in", + "enable_silent_payments_scanning": "Begin met het scannen van stille betalingen, totdat de tip is bereikt", "enabled": "Ingeschakeld", "enter_amount": "Voer Bedrag in", "enter_backup_password": "Voer hier een back-upwachtwoord in", @@ -295,6 +296,7 @@ "failed_authentication": "Mislukte authenticatie. ${state_error}", "faq": "FAQ", "features": "Functies", + "fee_rate": "Tarief", "fetching": "Ophalen", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balans", @@ -610,6 +612,7 @@ "send": "Sturen", "send_address": "${cryptoCurrency}-adres", "send_amount": "Bedrag:", + "send_change_to_you": "Verander, aan jou:", "send_creating_transaction": "Transactie maken", "send_error_currency": "Valuta kan alleen cijfers bevatten", "send_error_minimum_value": "Minimale waarde van bedrag is 0,01", @@ -676,6 +679,7 @@ "signature_invalid_error": "De handtekening is niet geldig voor het gegeven bericht", "signTransaction": "Transactie ondertekenen", "signup_for_card_accept_terms": "Meld je aan voor de kaart en accepteer de voorwaarden.", + "silent_payment": "Stille betaling", "silent_payments": "Stille betalingen", "silent_payments_always_scan": "Stel stille betalingen in het scannen", "silent_payments_disclaimer": "Nieuwe adressen zijn geen nieuwe identiteiten. Het is een hergebruik van een bestaande identiteit met een ander label.", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "Schakel over naar een EVM-compatibele portemonnee en probeer het opnieuw (Ethereum, Polygon)", "symbol": "Symbool", "sync_all_wallets": "Alle portemonnees synchroniseren", + "sync_status_attempting_scan": "Proberen scan", "sync_status_attempting_sync": "SYNCHRONISATIE PROBEREN", "sync_status_connected": "VERBONDEN", "sync_status_connecting": "AANSLUITING", "sync_status_failed_connect": "LOSGEKOPPELD", "sync_status_not_connected": "NIET VERBONDEN", - "sync_status_starting_scan": "Startscan", + "sync_status_starting_scan": "SCAN starten (van ${height})", "sync_status_starting_sync": "BEGINNEN MET SYNCHRONISEREN", "sync_status_syncronized": "SYNCHRONIZED", "sync_status_syncronizing": "SYNCHRONISEREN", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 85e75f40c..13976991a 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -232,8 +232,9 @@ "edit_token": "Edytuj token", "electrum_address_disclaimer": "Za każdym razem, gdy wykorzystasz adres, dla wiekszej prywatności generujemy nowy, ale poprzednie adresy nadal działają, i moga odbierać środki", "email_address": "Adres e-mail", + "enable_mempool_api": "Mempool API dla dokładnych opłat i dat", "enable_replace_by_fee": "Włącz wymianę po lewej", - "enable_silent_payments_scanning": "Włącz skanowanie cichych płatności", + "enable_silent_payments_scanning": "Zacznij skanować ciche płatności, aż do osiągnięcia wskazówki", "enabled": "Włączone", "enter_amount": "Wprowadź kwotę", "enter_backup_password": "Wprowadź tutaj hasło kopii zapasowej", @@ -295,6 +296,7 @@ "failed_authentication": "Nieudane uwierzytelnienie. ${state_error}", "faq": "FAQ", "features": "Cechy", + "fee_rate": "Stawka opłaty", "fetching": "Pobieranie", "fiat_api": "API Walut FIAT", "fiat_balance": "Bilans Fiata", @@ -610,6 +612,7 @@ "send": "Wyślij", "send_address": "Adres ${cryptoCurrency}", "send_amount": "Ilość:", + "send_change_to_you": "Zmień do ciebie:", "send_creating_transaction": "Tworzenie transakcji", "send_error_currency": "Waluta może zawierać tylko cyfry", "send_error_minimum_value": "Minimalna wartość to 0,01", @@ -676,6 +679,7 @@ "signature_invalid_error": "Podpis nie jest ważny dla podanej wiadomości", "signTransaction": "Podpisz transakcję", "signup_for_card_accept_terms": "Zarejestruj się, aby otrzymać kartę i zaakceptuj warunki.", + "silent_payment": "Cicha płatność", "silent_payments": "Ciche płatności", "silent_payments_always_scan": "Ustaw ciche płatności zawsze skanowanie", "silent_payments_disclaimer": "Nowe adresy nie są nową tożsamością. Jest to ponowne wykorzystanie istniejącej tożsamości z inną etykietą.", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "Przejdź na portfel zgodny z EVM i spróbuj ponownie (Ethereum, Polygon)", "symbol": "Symbol", "sync_all_wallets": "Synchronizuj wszystkie portfele", + "sync_status_attempting_scan": "Próba skanowania", "sync_status_attempting_sync": "PRÓBA SYNCHRONIZACJI", "sync_status_connected": "POŁĄCZONY", "sync_status_connecting": "ŁĄCZENIE", "sync_status_failed_connect": "POŁĄCZENIE NIEUDANE", "sync_status_not_connected": "NIE POŁĄCZONY", - "sync_status_starting_scan": "Rozpoczęcie skanowania", + "sync_status_starting_scan": "Rozpoczęcie skanowania (od ${height})", "sync_status_starting_sync": "ROZPOCZĘCIE SYNCHRONIZACJI", "sync_status_syncronized": "ZSYNCHRONIZOWANO", "sync_status_syncronizing": "SYNCHRONIZACJA", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 9283ca691..6f0f4f0b0 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -143,7 +143,7 @@ "confirm_fee_deduction": "Confirme dedução da taxa", "confirm_fee_deduction_content": "Você concorda em deduzir a taxa da saída?", "confirm_sending": "Confirmar o envio", - "confirm_silent_payments_switch_node": "Seu nó atual não suporta pagamentos silenciosos \\ Ncake Wallet mudará para um nó compatível, apenas para digitalização", + "confirm_silent_payments_switch_node": "Seu nó atual não suporta pagamentos silenciosos \n A Cake Wallet mudará para um nó compatível, apenas para escanear", "confirmations": "Confirmações", "confirmed": "Saldo Confirmado", "confirmed_tx": "Confirmado", @@ -232,8 +232,9 @@ "edit_token": "Editar símbolo", "electrum_address_disclaimer": "Geramos novos endereços cada vez que você usa um, mas os endereços anteriores continuam funcionando", "email_address": "Endereço de e-mail", + "enable_mempool_api": "Mempool API para taxas e datas precisas", "enable_replace_by_fee": "Habilite substituir por taxa", - "enable_silent_payments_scanning": "Ativar escaneamento de pagamentos silenciosos", + "enable_silent_payments_scanning": "Comece a escanear pagamentos silenciosos, até que o topo seja alcançada", "enabled": "Habilitado", "enter_amount": "Digite o valor", "enter_backup_password": "Digite a senha de backup aqui", @@ -295,6 +296,7 @@ "failed_authentication": "Falha na autenticação. ${state_error}", "faq": "FAQ", "features": "Funcionalidades", + "fee_rate": "Taxa de transação", "fetching": "Buscando", "fiat_api": "API da Fiat", "fiat_balance": "Equilíbrio Fiat", @@ -612,6 +614,7 @@ "send": "Enviar", "send_address": "Endereço ${cryptoCurrency}", "send_amount": "Montante:", + "send_change_to_you": "Troco, para você:", "send_creating_transaction": "Criando transação", "send_error_currency": "A moeda só pode conter números", "send_error_minimum_value": "O valor mínimo da quantia é 0,01", @@ -678,8 +681,9 @@ "signature_invalid_error": "A assinatura não é válida para a mensagem dada", "signTransaction": "Assinar transação", "signup_for_card_accept_terms": "Cadastre-se no cartão e aceite os termos.", + "silent_payment": "Pagamento silencioso", "silent_payments": "Pagamentos silenciosos", - "silent_payments_always_scan": "Defina pagamentos silenciosos sempre digitalizando", + "silent_payments_always_scan": "Defina pagamentos silenciosos sempre escaneando", "silent_payments_disclaimer": "Novos endereços não são novas identidades. É uma reutilização de uma identidade existente com um rótulo diferente.", "silent_payments_display_card": "Mostrar cartão de pagamento silencioso", "silent_payments_scan_from_date": "Escanear a partir da data", @@ -710,12 +714,13 @@ "switchToEVMCompatibleWallet": "Mude para uma carteira compatível com EVM e tente novamente (Ethereum, Polygon)", "symbol": "Símbolo", "sync_all_wallets": "Sincronize todas as carteiras", + "sync_status_attempting_scan": "TENTANDO ESCANEAR", "sync_status_attempting_sync": "TENTANDO SINCRONIZAR", "sync_status_connected": "CONECTADO", "sync_status_connecting": "CONECTANDO", "sync_status_failed_connect": "DESCONECTADO", "sync_status_not_connected": "DESCONECTADO", - "sync_status_starting_scan": "Diretor inicial", + "sync_status_starting_scan": "Começando scan (de ${height})", "sync_status_starting_sync": "INICIANDO SINCRONIZAÇÃO", "sync_status_syncronized": "SINCRONIZADO", "sync_status_syncronizing": "SINCRONIZANDO", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index bbdc9f745..5d600c3b4 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -232,8 +232,9 @@ "edit_token": "Изменить токен", "electrum_address_disclaimer": "Мы генерируем новые адреса каждый раз, когда вы их используете, но предыдущие адреса продолжают работать.", "email_address": "Адрес электронной почты", + "enable_mempool_api": "Mempool API за точные сборы и даты", "enable_replace_by_fee": "Включить замену за пикой", - "enable_silent_payments_scanning": "Включить сканирование безмолвных платежей", + "enable_silent_payments_scanning": "Начните сканировать безмолвные платежи, пока не будет достигнут наконечник", "enabled": "Включено", "enter_amount": "Введите сумму", "enter_backup_password": "Введите пароль резервной копии", @@ -295,6 +296,7 @@ "failed_authentication": "Ошибка аутентификации. ${state_error}", "faq": "FAQ", "features": "Функции", + "fee_rate": "Плата", "fetching": "Загрузка", "fiat_api": "Фиат API", "fiat_balance": "Фиатный баланс", @@ -611,6 +613,7 @@ "send": "Отправить", "send_address": "${cryptoCurrency} адрес", "send_amount": "Сумма:", + "send_change_to_you": "Изменить, для вас:", "send_creating_transaction": "Создать транзакцию", "send_error_currency": "Валюта может содержать только цифры", "send_error_minimum_value": "Mинимальная сумма 0.01", @@ -677,6 +680,7 @@ "signature_invalid_error": "Подпись недопустима для данного сообщения", "signTransaction": "Подписать транзакцию", "signup_for_card_accept_terms": "Подпишитесь на карту и примите условия.", + "silent_payment": "Молчаливый платеж", "silent_payments": "Молчаливые платежи", "silent_payments_always_scan": "Установить молчаливые платежи всегда сканирование", "silent_payments_disclaimer": "Новые адреса не являются новыми личностями. Это повторное использование существующей идентичности с другой этикеткой.", @@ -709,12 +713,13 @@ "switchToEVMCompatibleWallet": "Пожалуйста, переключитесь на кошелек, совместимый с EVM, и повторите попытку (Ethereum, Polygon).", "symbol": "Символ", "sync_all_wallets": "Синхронизировать все кошельки", + "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": "Начальное сканирование", + "sync_status_starting_scan": "Начальное сканирование (от ${height})", "sync_status_starting_sync": "НАЧАЛО СИНХРОНИЗАЦИИ", "sync_status_syncronized": "СИНХРОНИЗИРОВАН", "sync_status_syncronizing": "СИНХРОНИЗАЦИЯ", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 3161f60d7..f924c0d2b 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -232,8 +232,9 @@ "edit_token": "แก้ไขโทเค็น", "electrum_address_disclaimer": "เราสร้างที่อยู่ใหม่ทุกครั้งที่คุณใช้หนึ่งอย่าง แต่ที่อยู่เก่ายังสามารถใช้ได้ต่อไป", "email_address": "ที่อยู่อีเมล", + "enable_mempool_api": "Mempool API สำหรับค่าธรรมเนียมและวันที่ที่ถูกต้อง", "enable_replace_by_fee": "เปิดใช้งานการเปลี่ยนโดยค่าธรรมเนียม", - "enable_silent_payments_scanning": "เปิดใช้งานการสแกนการชำระเงินแบบเงียบ", + "enable_silent_payments_scanning": "เริ่มสแกนการชำระเงินแบบเงียบจนกว่าจะถึงปลาย", "enabled": "เปิดใช้งาน", "enter_amount": "กรอกจำนวน", "enter_backup_password": "ป้อนรหัสผ่านสำรองที่นี่", @@ -295,6 +296,7 @@ "failed_authentication": "การยืนยันสิทธิ์ล้มเหลว ${state_error}", "faq": "คำถามที่พบบ่อย", "features": "คุณสมบัติ", + "fee_rate": "อัตราค่าธรรมเนียม", "fetching": "กำลังโหลด", "fiat_api": "API สกุลเงินตรา", "fiat_balance": "เฟียต บาลานซ์", @@ -610,6 +612,7 @@ "send": "ส่ง", "send_address": "ที่อยู่ ${cryptoCurrency}", "send_amount": "จำนวน:", + "send_change_to_you": "เปลี่ยนเป็นคุณ:", "send_creating_transaction": "กำลังสร้างธุรกรรม", "send_error_currency": "สกุลเงินสามารถเป็นเลขเท่านั้น", "send_error_minimum_value": "จำนวนขั้นต่ำของจำนวนเงินคือ 0.01", @@ -676,6 +679,7 @@ "signature_invalid_error": "ลายเซ็นไม่ถูกต้องสำหรับข้อความที่ให้ไว้", "signTransaction": "ลงนามในการทำธุรกรรม", "signup_for_card_accept_terms": "ลงทะเบียนสำหรับบัตรและยอมรับเงื่อนไข", + "silent_payment": "การชำระเงินแบบเงียบ", "silent_payments": "การชำระเงินเงียบ", "silent_payments_always_scan": "ตั้งค่าการชำระเงินแบบเงียบเสมอ", "silent_payments_disclaimer": "ที่อยู่ใหม่ไม่ใช่ตัวตนใหม่ มันเป็นการใช้ซ้ำของตัวตนที่มีอยู่ด้วยฉลากที่แตกต่างกัน", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงินที่รองรับ EVM แล้วลองอีกครั้ง (Ethereum, Polygon)", "symbol": "เครื่องหมาย", "sync_all_wallets": "ซิงค์กระเป๋าเงินทั้งหมด", + "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": "เริ่มการสแกน", + "sync_status_starting_scan": "การสแกนเริ่มต้น (จาก ${height})", "sync_status_starting_sync": "กำลังเริ่มซิงโครไนซ์", "sync_status_syncronized": "ซิงโครไนซ์แล้ว", "sync_status_syncronizing": "กำลังซิงโครไนซ์", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 54c8c4c01..00a59b9a0 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -232,8 +232,9 @@ "edit_token": "I-edit ang token", "electrum_address_disclaimer": "Bumubuo kami ng mga bagong address sa tuwing gagamit ka ng isa, ngunit ang mga nakaraang address ay patuloy na gumagana", "email_address": "Email Address", + "enable_mempool_api": "Mempool API para sa tumpak na bayad at mga petsa", "enable_replace_by_fee": "Paganahin ang Replace-By-Fee", - "enable_silent_payments_scanning": "Paganahin ang pag-scan ng mga tahimik na pagbabayad", + "enable_silent_payments_scanning": "Simulan ang pag -scan ng tahimik na pagbabayad, hanggang sa maabot ang tip", "enabled": "Pinagana", "enter_amount": "Ipasok ang halaga", "enter_backup_password": "Ipasok ang backup na password dito", @@ -295,6 +296,7 @@ "failed_authentication": "Nabigo ang pagpapatunay. ${state_error}", "faq": "FAQ", "features": "Mga tampok", + "fee_rate": "Rate ng bayad", "fetching": "Pagkuha", "fiat_api": "Fiat API", "fiat_balance": "Balanse ng fiat", @@ -610,6 +612,7 @@ "send": "Ipadala", "send_address": "${cryptoCurrency} address", "send_amount": "Halaga:", + "send_change_to_you": "Baguhin, sa iyo:", "send_creating_transaction": "Paglikha ng transaksyon", "send_error_currency": "Ang halaga ay maaari lamang maglaman ng mga numero", "send_error_minimum_value": "Ang minimum na halaga ay 0.01", @@ -676,6 +679,7 @@ "signature_invalid_error": "Ang lagda ay hindi wasto para sa ibinigay na mensahe", "signTransaction": "Mag-sign ang Transaksyon", "signup_for_card_accept_terms": "Mag-sign up para sa card at tanggapin ang mga tuntunin.", + "silent_payment": "Tahimik na pagbabayad", "silent_payments": "Tahimik na pagbabayad", "silent_payments_always_scan": "Itakda ang mga tahimik na pagbabayad na laging nag-scan", "silent_payments_disclaimer": "Ang mga bagong address ay hindi mga bagong pagkakakilanlan. Ito ay isang muling paggamit ng isang umiiral na pagkakakilanlan na may ibang label.", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "Mangyaring lumipat sa isang EVM compatible na wallet at subukang muli (Ethereum, Polygon)", "symbol": "Simbolo", "sync_all_wallets": "I-sync ang lahat ng mga wallet", + "sync_status_attempting_scan": "Pagtatangka ng pag -scan", "sync_status_attempting_sync": "SINUSUBUKANG I-SYNC", "sync_status_connected": "KONEKTADO", "sync_status_connecting": "KUMOKENEKTA", "sync_status_failed_connect": "NADISKONEKTA", "sync_status_not_connected": "HINDI KONEKTADO", - "sync_status_starting_scan": "SIMULA SA PAG-SCAN", + "sync_status_starting_scan": "Simula sa pag -scan (mula sa ${height})", "sync_status_starting_sync": "SIMULA SA PAG-SYNC", "sync_status_syncronized": "NAKA-SYNCHRONIZE", "sync_status_syncronizing": "PAG-SYNCHRONIZE", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index dc0040002..700dd05b3 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -232,8 +232,9 @@ "edit_token": "Belirteci düzenle", "electrum_address_disclaimer": "Adresini her kullandığında yeni adres oluşturuyoruz, ancak önceki adresler de çalışmaya devam eder", "email_address": "E-posta Adresi", + "enable_mempool_api": "Doğru ücretler ve tarihler için Mempool API'si", "enable_replace_by_fee": "Farklı Değiştir'i Etkinleştir", - "enable_silent_payments_scanning": "Sessiz ödeme taramasını etkinleştirin", + "enable_silent_payments_scanning": "Bahşiş ulaşılıncaya kadar sessiz ödemeleri taramaya başlayın", "enabled": "Etkin", "enter_amount": "Miktar Girin", "enter_backup_password": "Yedekleme parolasını buraya gir", @@ -295,6 +296,7 @@ "failed_authentication": "Doğrulama başarısız oldu. ${state_error}", "faq": "SSS", "features": "Özellikler", + "fee_rate": "Ücret oranı", "fetching": "Getiriliyor", "fiat_api": "İtibari Para API", "fiat_balance": "Fiat Bakiyesi", @@ -610,6 +612,7 @@ "send": "Para Gönder", "send_address": "${cryptoCurrency} adresi", "send_amount": "Miktar:", + "send_change_to_you": "Değiştir, size:", "send_creating_transaction": "İşlem oluşturuluyor", "send_error_currency": "Para birimi sadece sayı içerebilir", "send_error_minimum_value": "Minimum tutar değeri 0.01'dir", @@ -676,6 +679,7 @@ "signature_invalid_error": "İmza verilen mesaj için geçerli değil", "signTransaction": "İşlem İmzala", "signup_for_card_accept_terms": "Kart için kaydol ve koşulları kabul et.", + "silent_payment": "Sessiz Ödeme", "silent_payments": "Sessiz ödemeler", "silent_payments_always_scan": "Sessiz ödemeleri her zaman tarama ayarlayın", "silent_payments_disclaimer": "Yeni adresler yeni kimlikler değildir. Farklı bir etikete sahip mevcut bir kimliğin yeniden kullanımıdır.", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "Lütfen EVM uyumlu bir cüzdana geçin ve tekrar deneyin (Ethereum, Polygon)", "symbol": "Sembol", "sync_all_wallets": "Tüm cüzdanları senkronize edin", + "sync_status_attempting_scan": "Tarama deneme", "sync_status_attempting_sync": "SENKRONİZE EDİLMEYE ÇALIŞILIYOR", "sync_status_connected": "BAĞLANILDI", "sync_status_connecting": "BAĞLANILIYOR", "sync_status_failed_connect": "BAĞLANTI KESİLDİ", "sync_status_not_connected": "BAĞLI DEĞİL", - "sync_status_starting_scan": "Başlangıç ​​taraması", + "sync_status_starting_scan": "Başlangıç ​​taraması (${height})", "sync_status_starting_sync": "SENKRONİZE BAŞLATILIYOR", "sync_status_syncronized": "SENKRONİZE EDİLDİ", "sync_status_syncronizing": "SENKRONİZE EDİLİYOR", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index a53d54492..8772f4ba3 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -232,8 +232,9 @@ "edit_token": "Редагувати маркер", "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати", "email_address": "Адреса електронної пошти", + "enable_mempool_api": "API Mempool для точних зборів та дат", "enable_replace_by_fee": "Увімкнути заміну з комісією", - "enable_silent_payments_scanning": "Увімкнути мовчазні платежі сканування", + "enable_silent_payments_scanning": "Почніть сканувати мовчазні платежі, поки не буде досягнуто наконечника", "enabled": "Увімкнено", "enter_amount": "Введіть суму", "enter_backup_password": "Введіть пароль резервної копії", @@ -295,6 +296,7 @@ "failed_authentication": "Помилка аутентифікації. ${state_error}", "faq": "FAQ", "features": "Особливості", + "fee_rate": "Ставка плати", "fetching": "Завантаження", "fiat_api": "Фіат API", "fiat_balance": "Фіат Баланс", @@ -611,6 +613,7 @@ "send": "Відправити", "send_address": "${cryptoCurrency} адреса", "send_amount": "Сума:", + "send_change_to_you": "Зміна, для вас:", "send_creating_transaction": "Створити транзакцію", "send_error_currency": "Валюта може містити тільки цифри", "send_error_minimum_value": "Мінімальна сума 0.01", @@ -677,6 +680,7 @@ "signature_invalid_error": "Підпис не є дійсним для наведеного повідомлення", "signTransaction": "Підписати транзакцію", "signup_for_card_accept_terms": "Зареєструйтеся на картку та прийміть умови.", + "silent_payment": "Мовчазний платіж", "silent_payments": "Мовчазні платежі", "silent_payments_always_scan": "Встановити мовчазні платежі завжди сканувати", "silent_payments_disclaimer": "Нові адреси - це не нові ідентичності. Це повторне використання існуючої ідентичності з іншою етикеткою.", @@ -709,12 +713,13 @@ "switchToEVMCompatibleWallet": "Перейдіть на гаманець, сумісний з EVM, і повторіть спробу (Ethereum, Polygon)", "symbol": "символ", "sync_all_wallets": "Синхронізувати всі гаманці", + "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": "Початок сканування", + "sync_status_starting_scan": "Початок сканування (від ${height})", "sync_status_starting_sync": "ПОЧАТОК СИНХРОНІЗАЦІЇ", "sync_status_syncronized": "СИНХРОНІЗОВАНИЙ", "sync_status_syncronizing": "СИНХРОНІЗАЦІЯ", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 65c2a768c..9b713900e 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -232,8 +232,9 @@ "edit_token": "ٹوکن میں ترمیم کریں۔", "electrum_address_disclaimer": "جب بھی آپ ایک کا استعمال کرتے ہیں تو ہم نئے پتے تیار کرتے ہیں، لیکن پچھلے پتے کام کرتے رہتے ہیں۔", "email_address": "ای میل اڈریس", + "enable_mempool_api": "درست فیسوں اور تاریخوں کے لئے میمپول API", "enable_replace_by_fee": "فی فیس کو تبدیل کریں", - "enable_silent_payments_scanning": "خاموش ادائیگیوں کو اسکیننگ کے قابل بنائیں", + "enable_silent_payments_scanning": "خاموش ادائیگیوں کو اسکین کرنا شروع کریں ، جب تک کہ نوک نہ پہنچ جائے", "enabled": "فعال", "enter_amount": "رقم درج کریں۔", "enter_backup_password": "یہاں بیک اپ پاس ورڈ درج کریں۔", @@ -295,6 +296,7 @@ "failed_authentication": "ناکام تصدیق۔ ${state_error}", "faq": "عمومی سوالات", "features": "خصوصیات", + "fee_rate": "فیس کی شرح", "fetching": "لا رہا ہے۔", "fiat_api": "Fiat API", "fiat_balance": "فیاٹ بیلنس", @@ -612,6 +614,7 @@ "send": "بھیجیں", "send_address": "${cryptoCurrency} پتہ", "send_amount": "رقم:", + "send_change_to_you": "آپ کو تبدیل کریں:", "send_creating_transaction": "لین دین کی تخلیق", "send_error_currency": "کرنسی صرف نمبروں پر مشتمل ہو سکتی ہے۔", "send_error_minimum_value": "رقم کی کم از کم قیمت 0.01 ہے۔", @@ -678,6 +681,7 @@ "signature_invalid_error": "دستخط دیئے گئے پیغام کے لئے درست نہیں ہے", "signTransaction": "۔ﮟﯾﺮﮐ ﻂﺨﺘﺳﺩ ﺮﭘ ﻦﯾﺩ ﻦﯿﻟ", "signup_for_card_accept_terms": "کارڈ کے لیے سائن اپ کریں اور شرائط کو قبول کریں۔", + "silent_payment": "خاموش ادائیگی", "silent_payments": "خاموش ادائیگی", "silent_payments_always_scan": "خاموش ادائیگی ہمیشہ اسکیننگ کریں", "silent_payments_disclaimer": "نئے پتے نئی شناخت نہیں ہیں۔ یہ ایک مختلف لیبل کے ساتھ موجودہ شناخت کا دوبارہ استعمال ہے۔", @@ -710,12 +714,13 @@ "switchToEVMCompatibleWallet": "(Ethereum, Polygon) ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ ﮯﻟﺍﻭ ﮯﻨﮭﮐﺭ ﺖﻘﺑﺎﻄﻣ ", "symbol": "ﺖﻣﻼﻋ", "sync_all_wallets": "تمام بٹوے کو ہم آہنگ کریں", + "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": "اسکین شروع کرنا", + "sync_status_starting_scan": "اسکین شروع کرنا (${height})", "sync_status_starting_sync": "مطابقت پذیری شروع کر رہا ہے۔", "sync_status_syncronized": "مطابقت پذیر", "sync_status_syncronizing": "مطابقت پذیری", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index d2c98fe2b..79c0fc519 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -140,8 +140,8 @@ "confirm": "Xác nhận", "confirm_delete_template": "Thao tác này sẽ xóa mẫu này. Bạn có muốn tiếp tục không?", "confirm_delete_wallet": "Thao tác này sẽ xóa ví này. Bạn có muốn tiếp tục không?", - "confirm_fee_deduction": "Xác nhận Khấu trừ Phí", "confirm_fee_dedction_content": "Bạn có đồng ý trừ phí từ đầu ra không?", + "confirm_fee_deduction": "Xác nhận Khấu trừ Phí", "confirm_sending": "Xác nhận gửi", "confirm_silent_payments_switch_node": "Nút hiện tại của bạn không hỗ trợ thanh toán im lặng\\nCake Wallet sẽ chuyển sang một nút tương thích chỉ để quét", "confirmations": "Xác nhận", @@ -230,6 +230,7 @@ "edit_token": "Chỉnh sửa token", "electrum_address_disclaimer": "Chúng tôi tạo địa chỉ mới mỗi khi bạn sử dụng, nhưng các địa chỉ cũ vẫn tiếp tục hoạt động", "email_address": "Địa chỉ Email", + "enable_mempool_api": "API Mempool cho các khoản phí và ngày chính xác", "enable_replace_by_fee": "Bật Thay thế Bằng Phí", "enable_silent_payments_scanning": "Bật quét thanh toán im lặng", "enabled": "Đã bật", @@ -298,7 +299,7 @@ "fiat_balance": "Số dư Fiat", "field_required": "Trường này là bắt buộc", "fill_code": "Vui lòng điền mã xác minh được gửi đến email của bạn", - "filter_by": "Lọc theo", + "filter_by": "Lọc theo", "first_wallet_text": "Ví tuyệt vời cho Monero, Bitcoin, Ethereum, Litecoin, và Haven", "fixed_pair_not_supported": "Cặp tỷ giá cố định này không được hỗ trợ với các sàn giao dịch đã chọn", "fixed_rate": "Tỷ giá cố định", @@ -398,7 +399,7 @@ "new_subaddress_label_name": "Tên nhãn", "new_subaddress_title": "Địa chỉ mới", "new_template": "Mẫu mới", - "new_wallet": "Ví mới", + "new_wallet": "Ví mới", "newConnection": "Kết nối mới", "no_cards_found": "Không tìm thấy thẻ", "no_id_needed": "Không cần ID!", @@ -498,7 +499,7 @@ "red_dark_theme": "Chủ đề tối đỏ", "red_light_theme": "Chủ đề sáng đỏ", "redeemed": "Đã đổi", - "refund_address": "Địa chỉ hoàn tiền", + "refund_address": "Địa chỉ hoàn tiền", "reject": "Từ chối", "remaining": "còn lại", "remove": "Gỡ bỏ", @@ -598,7 +599,7 @@ "seedtype": "Loại hạt giống", "seedtype_legacy": "Di sản (25 từ)", "seedtype_polyseed": "Polyseed (16 từ)", - "seedtype_wownero": "Wownero (14 từ)", + "seedtype_wownero": "Wownero (14 từ)", "select_backup_file": "Chọn tệp sao lưu", "select_buy_provider_notice": "Chọn nhà cung cấp mua ở trên. Bạn có thể bỏ qua màn hình này bằng cách thiết lập nhà cung cấp mua mặc định trong cài đặt ứng dụng.", "select_destination": "Vui lòng chọn đích cho tệp sao lưu.", @@ -698,7 +699,7 @@ "support_description_guides": "Tài liệu và hỗ trợ cho các vấn đề phổ biến", "support_description_live_chat": "Miễn phí và nhanh chóng! Các đại diện hỗ trợ được đào tạo sẵn sàng hỗ trợ", "support_description_other_links": "Tham gia cộng đồng của chúng tôi hoặc liên hệ với chúng tôi hoặc các đối tác của chúng tôi qua các phương pháp khác", - "support_title_guides": "Hướng dẫn Cake Wallet", + "support_title_guides": "Hướng dẫn Cake Wallet", "support_title_live_chat": "Hỗ trợ trực tiếp", "support_title_other_links": "Liên kết hỗ trợ khác", "sweeping_wallet": "Quét ví", @@ -712,7 +713,7 @@ "sync_status_connecting": "ĐANG KẾT NỐI", "sync_status_failed_connect": "ĐÃ NGẮT KẾT NỐI", "sync_status_not_connected": "CHƯA KẾT NỐI", - "sync_status_starting_scan": "ĐANG BẮT ĐẦU QUÉT", + "sync_status_starting_scan": "ĐANG BẮT ĐẦU QUÉT (${height})", "sync_status_starting_sync": "ĐANG BẮT ĐẦU ĐỒNG BỘ", "sync_status_syncronized": "ĐÃ ĐỒNG BỘ", "sync_status_syncronizing": "ĐANG ĐỒNG BỘ", @@ -798,7 +799,7 @@ "trongrid_history": "Lịch sử TronGrid", "trusted": "Đã tin cậy", "tx_commit_exception_no_dust_on_change": "Giao dịch bị từ chối với số tiền này. Với số tiền này bạn có thể gửi ${min} mà không cần đổi tiền lẻ hoặc ${max} trả lại tiền lẻ.", - "tx_commit_failed": "Giao dịch không thành công. Vui lòng liên hệ với hỗ trợ.", + "tx_commit_failed": "Giao dịch không thành công. Vui lòng liên hệ với hỗ trợ.", "tx_invalid_input": "Bạn đang sử dụng loại đầu vào sai cho loại thanh toán này", "tx_no_dust_exception": "Giao dịch bị từ chối vì gửi một số tiền quá nhỏ. Vui lòng thử tăng số tiền.", "tx_not_enough_inputs_exception": "Không đủ đầu vào có sẵn. Vui lòng chọn thêm dưới Coin Control", @@ -897,4 +898,4 @@ "you_will_get": "Chuyển đổi thành", "you_will_send": "Chuyển đổi từ", "yy": "YY" -} +} \ No newline at end of file diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index a5ef82d62..fbea1ba84 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -233,8 +233,9 @@ "edit_token": "Ṣatunkọ àmi", "electrum_address_disclaimer": "A dá àwọn àdírẹ́sì títun ní gbogbo àwọn ìgbà t'ẹ́ lo ó kan ṣùgbọ́n ẹ lè tẹ̀síwájú lo àwọn àdírẹ́sì tẹ́lẹ̀tẹ́lẹ̀.", "email_address": "Àdírẹ́sì ímeèlì", + "enable_mempool_api": "Mempool API fun awọn owo deede ati awọn ọjọ", "enable_replace_by_fee": "Mu ki o rọpo", - "enable_silent_payments_scanning": "Mu ki awọn sisanwo ipalọlọ", + "enable_silent_payments_scanning": "Bẹrẹ awọn sisanwo ipalọlọ, titi ti o fi de opin", "enabled": "Wọ́n tíwọn ti tan", "enter_amount": "Tẹ̀ iye", "enter_backup_password": "Tẹ̀ ọ̀rọ̀ aṣínà ti ẹ̀dà ḿbí", @@ -296,6 +297,7 @@ "failed_authentication": "Ìfẹ̀rílàdí pipòfo. ${state_error}", "faq": "Àwọn ìbéèrè l'a máa ń bèèrè", "features": "Awọn ẹya", + "fee_rate": "Oṣuwọn owo ọya", "fetching": "ń wá", "fiat_api": "Ojú ètò áàpù owó tí ìjọba pàṣẹ wa lò", "fiat_balance": "Fiat Iwontunws.funfun", @@ -611,6 +613,7 @@ "send": "Ránṣẹ́", "send_address": "${cryptoCurrency} àdírẹ́sì", "send_amount": "Iye:", + "send_change_to_you": "Yipada, si ọ:", "send_creating_transaction": "Ńṣe àránṣẹ́", "send_error_currency": "Ó yẹ kí òǹkà dá wà nínu iye", "send_error_minimum_value": "Ránṣẹ́ owó kò kéré dé 0.01", @@ -677,6 +680,7 @@ "signature_invalid_error": "Ibuwọlu ko wulo fun ifiranṣẹ ti a fun", "signTransaction": "Wole Idunadura", "signup_for_card_accept_terms": "Ẹ f'orúkọ sílẹ̀ láti gba káàdì àti àjọrò.", + "silent_payment": "Isanwo dakẹ", "silent_payments": "Awọn sisanwo ipalọlọ", "silent_payments_always_scan": "Ṣeto awọn sisanwo ipalọlọ nigbagbogbo n ṣatunṣe", "silent_payments_disclaimer": "Awọn adirẹsi tuntun kii ṣe awọn idanimọ tuntun. O jẹ yiyan ti idanimọ ti o wa pẹlu aami oriṣiriṣi.", @@ -709,12 +713,13 @@ "switchToEVMCompatibleWallet": "Jọwọ yipada si apamọwọ ibaramu EVM ki o tun gbiyanju lẹẹkansi (Ethereum, Polygon)", "symbol": "Aami", "sync_all_wallets": "Muṣiṣẹpọ gbogbo awọn Woleti", + "sync_status_attempting_scan": "Igbiyanju ọlọjẹ", "sync_status_attempting_sync": "Ń GBÌYÀNJÚ MÚDỌ́GBA", "sync_status_connected": "TI DÁRAPỌ̀ MỌ́", "sync_status_connecting": "Ń DÁRAPỌ̀ MỌ́", "sync_status_failed_connect": "ÌKÀNPỌ̀ TI KÚ", "sync_status_not_connected": "KÒ TI DÁRAPỌ̀ MỌ́ Ọ", - "sync_status_starting_scan": "Bibẹrẹ ọlọjẹ", + "sync_status_starting_scan": "Ibẹrẹ ọlọjẹ (lati ${height})", "sync_status_starting_sync": "Ń BẸ̀RẸ̀ RẸ́", "sync_status_syncronized": "TI MÚDỌ́GBA", "sync_status_syncronizing": "Ń MÚDỌ́GBA", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 37e760de2..2c0abe5eb 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -232,8 +232,9 @@ "edit_token": "编辑令牌", "electrum_address_disclaimer": "每次您使用一个地址时,我们都会生成新地址,但之前的地址仍然有效", "email_address": "电子邮件地址", + "enable_mempool_api": "Mempool API获得准确的费用和日期", "enable_replace_by_fee": "启用by-Fee替换", - "enable_silent_payments_scanning": "启用无声付款扫描", + "enable_silent_payments_scanning": "开始扫描无声付款,直到达到提示", "enabled": "启用", "enter_amount": "输入金额", "enter_backup_password": "在此处输入備用密码", @@ -295,6 +296,7 @@ "failed_authentication": "身份验证失败. ${state_error}", "faq": "FAQ", "features": "特征", + "fee_rate": "费率", "fetching": "正在获取", "fiat_api": "法币API", "fiat_balance": "法币余额", @@ -610,6 +612,7 @@ "send": "发送", "send_address": "${cryptoCurrency} 地址", "send_amount": "金额:", + "send_change_to_you": "改变,向您:", "send_creating_transaction": "创建交易", "send_error_currency": "货币只能包含数字", "send_error_minimum_value": "最小金额为0.01", @@ -676,6 +679,7 @@ "signature_invalid_error": "签名对于给出的消息无效", "signTransaction": "签署交易", "signup_for_card_accept_terms": "注册卡并接受条款。", + "silent_payment": "无声付款", "silent_payments": "无声付款", "silent_payments_always_scan": "设置无声付款总是扫描", "silent_payments_disclaimer": "新地址不是新的身份。这是重复使用具有不同标签的现有身份。", @@ -708,12 +712,13 @@ "switchToEVMCompatibleWallet": "请切换到 EVM 兼容钱包并重试(以太坊、Polygon)", "symbol": "象征", "sync_all_wallets": "同步所有钱包", + "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": "开始扫描", + "sync_status_starting_scan": "启动扫描(来自 ${height})", "sync_status_starting_sync": "开始同步", "sync_status_syncronized": "已同步", "sync_status_syncronizing": "正在同步", diff --git a/tool/configure.dart b/tool/configure.dart index 1d2166ed6..f77db8d1c 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -116,7 +116,7 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_core/get_height_by_date.dart'; -import 'package:cw_bitcoin/script_hash.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart'; import 'package:mobx/mobx.dart'; """; @@ -211,7 +211,8 @@ abstract class Bitcoin { int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, {int? outputsCount, int? size}); int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, {int? size}); - int getHeightByDate({required DateTime date}); + Future checkIfMempoolAPIIsEnabled(Object wallet); + Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}); Future rescan(Object wallet, {required int height, bool? doSingleScan}); Future getNodeIsElectrsSPEnabled(Object wallet); void deleteSilentPaymentAddress(Object wallet, String address); @@ -220,6 +221,8 @@ abstract class Bitcoin { void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + List updateOutputs(PendingTransaction pendingTransaction, List outputs); + bool txIsReceivedSilentPayment(TransactionInfo txInfo); } """; From 4e2e5e708c1d6cea0e4fe25444748c6468b974c6 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 20 Sep 2024 19:25:08 +0100 Subject: [PATCH 083/142] CW-580: BIP39 Wallets Shared Seed Implementation: "One Seed - Multiple Wallets" (#1307) * feat: Implement creating new BIP39 wallet with same seed used for other owned BIP39 wallets * feat: Use same seed for BIP39 Wallets * Update pre_existing_seeds_page.dart * Feat: BIP39 Same seed wallet creation using the Common Parent Wallet Strategy * feat: Finalize implementing preexisting seeds * feat: Implement shared bip39 wallet seed for Bitcoin wallet type * feat: Implement shared bip39 wallet seed for Litecoin wallet type * feat: Implement shared bip39 wallet seed for BitcoinCash wallet type * feat: Implement shared bip39 wallet seed for Nano wallet type, although disabled entry for now * fix: Remove non bip39 seed wallet type from listing * feat: Implement grouped and single wallets lists in wallets listing page and implement editing and saving group names * fix: Issue where the ontap always references the leadwallet, also make shared seed wallets section header only display when the multi wallet groups list is not empty * fix: Add translation and adjust the way the groups display * feat: Activate bip39 as an option for creating Nano wallet types * fix: Handle edgecase with creating new wallet with group address, handle case where only bip39 derivation type is allowed with child wallets, activate nano wallet type for shared seed * chore: Modify the UI to fit adjustment made on figma * fix: Disposed box triggering error in hive and causing wallet list view to display error * fix: Switch wallet groups title in wallets list page and also fix issue with renaming groups * Update lib/reactions/bip39_wallet_utils.dart [skip ci] Co-authored-by: Omar Hatem * Update lib/router.dart [skip ci] Co-authored-by: Omar Hatem * fix: Review fixes: Combine New Wallet Page Type arguments into a single model * fix: Review fixes: Add failure guard when fetching mnemonic for selected wallet in pre-existing wallets page * fix: Review fixes - Add loading indicator when mnemonic is being selected for wallet * fix: Review fixes - Modify variable name to avoid clashes * fix: Review fixes - Access WalletManager through dependency injection instead of service location * fix: Review fixes - Add testnet to convertWalletInfoToWalletlistItem function, and adjust according where used * fix: Review fixes - Add walletPassword to nano, tron and wownero wallets and confirm it is properly handled as it should be * fix: Remove leadWallet, modify filtering flow to reflect this and not depend on leadWallet, and adjust privacy settings * fix: Review Fixes - Modify restore flow to reflect current nature of bip39 as default for majority of wallet types * fix: QA Fixes - Modify preexisting page to display wallet group names if set, and display them in incremental order if not set * fix: Add wallet group description page and rename pre-existingseeds page to wallet group display page * fix: Product Fix - Rename pre-existing seeds file name to wallet group display filename * fix: Product fix - Separate multiwallets groups from single wallets and display separately * fix - Product Fix - Add empty state for wallet group listing when creating a new wallet, adjust CTAs across buttons relating to the flow also --------- Co-authored-by: Omar Hatem --- assets/images/wallet_group.png | Bin 0 -> 5053 bytes .../bitcoin_wallet_creation_credentials.dart | 5 + cw_bitcoin/lib/bitcoin_wallet_service.dart | 2 +- cw_bitcoin/lib/litecoin_wallet_service.dart | 4 +- ...coin_cash_wallet_creation_credentials.dart | 18 +- .../lib/src/bitcoin_cash_wallet_service.dart | 2 +- cw_core/lib/wallet_credentials.dart | 2 + cw_core/lib/wallet_info.dart | 6 + cw_ethereum/lib/ethereum_wallet_service.dart | 2 +- ...evm_chain_wallet_creation_credentials.dart | 16 +- .../lib/nano_wallet_creation_credentials.dart | 8 +- cw_nano/lib/nano_wallet_service.dart | 20 +- cw_polygon/lib/polygon_wallet_service.dart | 2 +- .../solana_wallet_creation_credentials.dart | 15 +- cw_solana/lib/solana_wallet_service.dart | 2 +- .../lib/tron_wallet_creation_credentials.dart | 16 +- cw_tron/lib/tron_wallet_service.dart | 2 +- ios/Podfile.lock | 55 +--- lib/bitcoin/cw_bitcoin.dart | 18 +- lib/bitcoin_cash/cw_bitcoin_cash.dart | 10 +- lib/core/new_wallet_arguments.dart | 15 + lib/core/new_wallet_type_arguments.dart | 14 + lib/di.dart | 74 +++-- lib/entities/preferences_key.dart | 1 + lib/entities/seed_type.dart | 25 ++ lib/entities/wallet_edit_page_arguments.dart | 28 ++ lib/entities/wallet_group.dart | 21 ++ lib/entities/wallet_manager.dart | 110 ++++++++ lib/ethereum/cw_ethereum.dart | 10 +- lib/nano/cw_nano.dart | 7 +- lib/polygon/cw_polygon.dart | 49 ++-- lib/reactions/bip39_wallet_utils.dart | 21 ++ lib/router.dart | 130 ++++++--- lib/routes.dart | 9 +- lib/solana/cw_solana.dart | 10 +- .../desktop_wallet_selection_dropdown.dart | 4 +- .../advanced_privacy_settings_page.dart | 83 +++++- .../screens/new_wallet/new_wallet_page.dart | 26 +- .../new_wallet/new_wallet_type_page.dart | 49 +++- .../wallet_group_description_page.dart | 90 ++++++ .../new_wallet/wallet_group_display_page.dart | 192 +++++++++++++ .../grouped_wallet_expansion_tile.dart | 119 ++++++++ .../new_wallet/widgets/select_button.dart | 10 +- .../screens/restore/wallet_restore_page.dart | 9 +- lib/src/screens/wallet/wallet_edit_page.dart | 109 ++++---- .../edit_wallet_button_widget.dart | 54 ++++ .../screens/wallet_list/filtered_list.dart | 3 + .../screens/wallet_list/wallet_list_page.dart | 256 ++++++++++-------- lib/store/settings_store.dart | 22 ++ lib/tron/cw_tron.dart | 15 +- .../advanced_privacy_settings_view_model.dart | 19 +- .../new_wallet_type_view_model.dart | 16 ++ lib/view_model/seed_settings_view_model.dart | 7 + lib/view_model/wallet_creation_vm.dart | 17 +- .../wallet_groups_display_view_model.dart | 165 +++++++++++ .../wallet_list/wallet_edit_view_model.dart | 33 ++- .../wallet_list/wallet_list_view_model.dart | 53 +++- lib/view_model/wallet_new_vm.dart | 81 ++++-- res/values/strings_ar.arb | 15 +- res/values/strings_bg.arb | 14 + res/values/strings_cs.arb | 14 + res/values/strings_de.arb | 14 + res/values/strings_en.arb | 14 + res/values/strings_es.arb | 14 + res/values/strings_fr.arb | 14 + res/values/strings_ha.arb | 14 + res/values/strings_hi.arb | 14 + res/values/strings_hr.arb | 14 + res/values/strings_hy.arb | 14 + res/values/strings_id.arb | 14 + res/values/strings_it.arb | 14 + res/values/strings_ja.arb | 14 + res/values/strings_ko.arb | 14 + res/values/strings_my.arb | 14 + res/values/strings_nl.arb | 14 + res/values/strings_pl.arb | 14 + res/values/strings_pt.arb | 14 + res/values/strings_ru.arb | 14 + res/values/strings_th.arb | 14 + res/values/strings_tl.arb | 30 +- res/values/strings_tr.arb | 14 + res/values/strings_uk.arb | 14 + res/values/strings_ur.arb | 14 + res/values/strings_vi.arb | 8 + res/values/strings_yo.arb | 14 + res/values/strings_zh.arb | 14 + tool/configure.dart | 18 +- 87 files changed, 2152 insertions(+), 430 deletions(-) create mode 100644 assets/images/wallet_group.png create mode 100644 lib/core/new_wallet_arguments.dart create mode 100644 lib/core/new_wallet_type_arguments.dart create mode 100644 lib/entities/wallet_edit_page_arguments.dart create mode 100644 lib/entities/wallet_group.dart create mode 100644 lib/entities/wallet_manager.dart create mode 100644 lib/reactions/bip39_wallet_utils.dart create mode 100644 lib/src/screens/new_wallet/wallet_group_description_page.dart create mode 100644 lib/src/screens/new_wallet/wallet_group_display_page.dart create mode 100644 lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart create mode 100644 lib/src/screens/wallet_list/edit_wallet_button_widget.dart create mode 100644 lib/view_model/new_wallet_type_view_model.dart create mode 100644 lib/view_model/wallet_groups_display_view_model.dart diff --git a/assets/images/wallet_group.png b/assets/images/wallet_group.png new file mode 100644 index 0000000000000000000000000000000000000000..f4a577b22a05a172de08fc32b178dc3317047526 GIT binary patch literal 5053 zcmai2g;Nv`(?&oAjt(8WI8-=jXlQtHvXbimbozh9dht)X8t;7jr?4Gl^_
|GG>K1DW2bVv_sRl z2zo$J|3`5tDcbZ}A3q5-27(hJT7aau>StO-oea2@+0Tah&tAJDT4TWa~Qodu6`P1QC_OT&y_N#DlaGQZdc1bq3G7%lM)s zuwH}&GY-gCwyxb3gqp8F$hg?`>9otCd}@`S%ldRy4zJC1QCW)+* z28g$Iy{JWDUsvR0-(mO6ug7sTZOAcZ%~)vJ-hFJ^P_NVEFfju$61LXyJpRR0OJmlc zju3;#YTw=OWXrjHh+XcM!Hl3%UQQs%bv-hd$34+&x_Rrko4;XRhWPdvEBW&$*whCS zkp^M$_TG*HGO6qSCUZ>zC)vSZuTC_!Jmp&HvG;K*w@QoSX&Ove0wF*-%H${W`%4kz zbcImT%F~C$LY(!laJ^~|^LcYN;V!!hP%xL}z)rj=x5Gm#L~-k?;yVU#Nd2~ZPswF+IkNNfSkMcp~FUJ3f=OOE7V_lL~6|Inf9di7XI z_l&r<+%fj-1#Az?p5;k+l_B}J-$O+(-Zo#yhxz0(W?<{APB9rE*IRP>NtF2&yg@Hki<_4DP%Fjst zmZv7r*1mlN%xB_MwH|DrcPZgBBI`SUyf9folnYS=*XMe8zt*E?rDM`T;DqvZ$ zkt0&-Q^$P`qYFm#q+DucaZnTehITJIT|PWi1<$0qfGiJsCHE@v1Lyh4u=iWterTqr zALXU?9ucz?pzn_qm98W{b=m#8SCbmbp?e>A{zK4X}gTI+Y(pYau~PV|KPL`@1R3i{$sy(qFjaBOs+O{!*zx3$wU$6`4mgez9l!) z#80JDh8$YsJpZNlB)gG^t;4K|t1-R482eK-W$1}cI=Hl9M1Na)*u4C&*!tB``x~Ut z8;lpK#^~2&zbyDf1`|w`Pzt(oZT+jhhM&HP6(-`IeBl)CDH#wE6?%{bsP<;RjFQbjYf za?`TZ6Di1}-hem#ZkCI$ZDTsW~0~ z*zCIt3VL+tOd;ex&8RwuZ%NwfgLM`T_W6?->1zWUqp^$=n=RAFt**|JXu@Lircf?0 z3>hAwmN&@6XdboCI5tA9F@uib+R%^Zl4xuC&@`!{>Gi+CahJ-1HB>F?T5+?lcfEg-SLKTsxOAc7LJv z&l#c zE+{{gyuq~{`IB7fB(Umwa4@4*k%ue%Oz10r)y65d&2{|7Xt2~MuWfp^ek_fgMr{t# zViMP%omGuYuEg{SG{07#>(YI-qjq`6VuVyCY&8(Zef_+j8B`!mKQpCR0?<~Htb0Y1IHA&1+;s5(+4`hO9c4I{LiPZh$oBW zO3crwtN#^!uvEPk@lTc9FpeI{Yec+&A{W| zg1}q+MSd1qvbVYzr%5Igi^}K2rx%kFU=_8}QugaR9nbDjwze!Vr%qSH%NdZYNlNda zr^G$!;VQ23IvkF@w-bCAfio}W#_^C-eF^=3)_}-vVk04_fVip%F(*?39(73D8Qs3r z6{bvn_4zgzrKA^uFz$Z;cSl=rXd#IWmw$JhS_ET-Ugzu1UChc%X&b>>_t<#($08Ya@~f)$wa!j9=18+OAbta)sql?o z5A2e{|8TqVN3!5{Z*m%ORD7N|8pdn@&mE0eoW;Ud-H$ zoBJn9yU1CkzHepocgy4Y-ju6m=i4d3^&< z3h$Xw&^osTY)OWoJz5|9jg`tEyb~Og9Q6sOWY|jgFAZG`Ff>)Tqa1ffJC9u_>1EyL zf((V)p%QI{WgddEJ7J?3Y?{bA?%YhO6gS6rhYun5Hq^z!;Hp z3Zvh3WszY#GlV~N;&&a0PKd0Mnect2P9cVH&Pj}6S2jz!R2Qbu=wmQe`R9z;)=ArX z#v(=3yDQ}<0pyp{=||G-A7&g^y-G21`}KEunOQYjPDggPfyv`C;q>#bz4*Tb{(AYh zPEB5=D5=XaWmpuUEiZz)5lxI=qF`_ND5b;&t`%aE%@}nQf>z2P^cq;VL_2M33WlE* z6RJ%U)0Ov60}esyJIm5q_<`R!Sr@y20U9a&5Eb`=f}@A)nU6ukbM*l)MNNQ5VL`(g z1cD3#Qo<8JqUt^D0lbQ_%7}La@)-G&r)k zB*H~8B*yh!bgJj>mxFGPdgY`+%a`kGLtB_@u$Y*ryCLRW4J)`0j3B~nkgfbm6PI<) z-gb*(KK^7g`q*d&y+F|#{t0V!TA@15Uew(n5o#|%eCMRle z%hi_VzV{T-ZFX6Af|2reDh@v3dp`>7?3)1)G6&ajh&32IXsCZHOT=E|oKA3cob$>l zJ>?ERFqP6j;-%>64#5zIP8RhGX~)bOIFr$~chRA*ov8-@tf1H~M(9~pZ6|cQebNJ= zGGJKZrjCB))a3sMft?im13oY?on2rGJbK-L@-+}Jr+$0rxe2>?wo5d!O%JvyHopxO zC@D6)KRxb%THx34dpp-06E`6tDtx6>XD*KL)$UMPP)JVc-N3ZiP4SITN%QqQOA&d3f<&6lO^C)eQ)QLt z{#z|v=;Mm{SkIW^!Z*9JlVm&#gvD__l|ggK|!ast-X>_cGh~r<56HYxg&-k#@B% zHT5=|Ay+ohJqMYRnAd02!%=n8R%QQTJFpbW&(v1I@}*WL1c7 z=k09{@zBt;{HGv7J*!vm70--lW4G{mg!=O}BDEA$3|A9bQg7ZGzGR38gJ9HyBu2QV zRrz+`uA;s6m*h3I3KVcSXpg@|K1r#9g`AuOdMDVJ>ChUR56o8H!Rc zui|q4z&-fNf6WG;l%DdihplYXbw#uMns0{}oQDt6t_{2^K+XA6hpp;~iUCENmWY%kavD8wi(IHE{om+%6ZP%JC+-~~=Gvq26Ys`4j_?wc+lPTx@0$0e@Ey}` z>Ez*{P114Kz1y0W-JO($<1 z(mHo|i+PlHgH`&u&o|fJP1lEpo&(`No0>t8*e{Za5pr1JU2hTQ)_CRiHT|4$%M}L? zx5OXisUETXR{?ZzKgN^FKkvqxFM)+kk{E$=@}N3DcPa5<0ouCEY#WEzWN*T7o5vBa zlw#jz2%dt+ye)-!J>GqGx?V)}{cLFhUhC@kp^E7KRP}*@d@A0*GZZx7_%dv9q-hE1 zbZ=h+QRKpIUWo-Q6NzqQ5y?DwfDuF2<1~(<)$Jeb?A=S*8N4gj93re?YkQBpJS{Sf zmTZ<*yE9yeJ#l^0wQ)(V_rx-|815(bNSIV~Zj3{8IW6hkNN-V}j5j_=I5hOd3V^f0 z7Yj?f`bk9y*zfqjZD5=H9y%@J5b?5K`zAXFvL~>mq)~0un9Vzbl#RMwatM+??l0D! z)suf7y=H;v+}nKY$9vr*h{>E_Mf&+6hV$j@mybCfG))YY_6 zEqu<-b7|oq1v!`TP~(iT(4w!6EK0ztm_Sf0iiwPK zY}GE-_I`95|N0W5D*V@I1S{$qO~{tNk(9{daFcxChPvdZ@@s4A2>^#c<3hAx^VsT+t1*6Rlq^3*|wKZQ|UBx;Ab>U{H!386gJx?Cu%GGn2(Q|hH7k}7oCV_wjF4paIhMsU>i0f%->e!&|-=bqg!*MUbd2Y*V`*x_wJ z1E$ckZhMIYB(22!vRb+xyLS1BuG0gpf4fdVRZg{$#HSUsWf=U<-a%#4WO1AZ^4gf) zEkOv4?qu%33^yym9WsNLp5U^HDM9#uBUtnC8EuK=Bmoz<+P`lbnw*rfBogvD@PFnn B*q8tS literal 0 HcmV?d00001 diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 5c276390a..a1b1418b8 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -10,12 +10,17 @@ class BitcoinNewWalletCredentials extends WalletCredentials { DerivationType? derivationType, String? derivationPath, String? passphrase, + this.mnemonic, + String? parentAddress, }) : super( name: name, walletInfo: walletInfo, password: password, passphrase: passphrase, + parentAddress: parentAddress, ); + + final String? mnemonic; } class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 9f67f7807..06f2082e4 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -41,7 +41,7 @@ class BitcoinWalletService extends WalletService< case DerivationType.bip39: final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - mnemonic = await MnemonicBip39.generate(strength: strength); + mnemonic = credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength); break; case DerivationType.electrum: default: diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index c13265934..e42669402 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -32,11 +32,11 @@ class LitecoinWalletService extends WalletService< @override Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { final String mnemonic; - switch ( credentials.walletInfo?.derivationInfo?.derivationType) { + switch (credentials.walletInfo?.derivationInfo?.derivationType) { case DerivationType.bip39: final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - mnemonic = await MnemonicBip39.generate(strength: strength); + mnemonic = credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength); break; case DerivationType.electrum: default: diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart index bb93656f1..af93cdbf8 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart @@ -2,9 +2,21 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class BitcoinCashNewWalletCredentials extends WalletCredentials { - BitcoinCashNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password, String? passphrase}) - : super(name: name, walletInfo: walletInfo, password: password, passphrase: passphrase); + BitcoinCashNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + String? passphrase, + this.mnemonic, + String? parentAddress, + }) : super( + name: name, + walletInfo: walletInfo, + password: password, + passphrase: passphrase, + parentAddress: parentAddress + ); + final String? mnemonic; } class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index f004356b5..ab5117aa8 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -36,7 +36,7 @@ class BitcoinCashWalletService extends WalletService< final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final wallet = await BitcoinCashWalletBase.create( - mnemonic: await MnemonicBip39.generate(strength: strength), + mnemonic: credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength), password: credentials.password!, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, diff --git a/cw_core/lib/wallet_credentials.dart b/cw_core/lib/wallet_credentials.dart index 30ae2546c..55c24bf37 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -10,6 +10,7 @@ abstract class WalletCredentials { this.passphrase, this.derivationInfo, this.hardwareWalletType, + this.parentAddress, }) { if (this.walletInfo != null && derivationInfo != null) { this.walletInfo!.derivationInfo = derivationInfo; @@ -18,6 +19,7 @@ abstract class WalletCredentials { final String name; final int? height; + String? parentAddress; int? seedPhraseLength; String? password; String? passphrase; diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index ff0c011bb..0b0e8e4c5 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -80,6 +80,7 @@ class WalletInfo extends HiveObject { this.showIntroCakePayCard, this.derivationInfo, this.hardwareWalletType, + this.parentAddress, ) : _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external({ @@ -97,6 +98,7 @@ class WalletInfo extends HiveObject { String yatLastUsedAddressRaw = '', DerivationInfo? derivationInfo, HardwareWalletType? hardwareWalletType, + String? parentAddress, }) { return WalletInfo( id, @@ -113,6 +115,7 @@ class WalletInfo extends HiveObject { showIntroCakePayCard, derivationInfo, hardwareWalletType, + parentAddress, ); } @@ -184,6 +187,9 @@ class WalletInfo extends HiveObject { @HiveField(21) HardwareWalletType? hardwareWalletType; + @HiveField(22) + String? parentAddress; + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 84fc0a277..51d6ecf42 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -21,7 +21,7 @@ class EthereumWalletService extends EVMChainWalletService { Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - final mnemonic = bip39.generateMnemonic(strength: strength); + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); final wallet = EthereumWallet( walletInfo: credentials.walletInfo!, diff --git a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart index e8a13cbb9..d7a33f6b3 100644 --- a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart +++ b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart @@ -3,8 +3,20 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class EVMChainNewWalletCredentials extends WalletCredentials { - EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) - : super(name: name, walletInfo: walletInfo, password: password); + EVMChainNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + String? parentAddress, + this.mnemonic, + }) : super( + name: name, + walletInfo: walletInfo, + password: password, + parentAddress: parentAddress, + ); + + final String? mnemonic; } class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_nano/lib/nano_wallet_creation_credentials.dart b/cw_nano/lib/nano_wallet_creation_credentials.dart index 4ee79ce48..9958fad7c 100644 --- a/cw_nano/lib/nano_wallet_creation_credentials.dart +++ b/cw_nano/lib/nano_wallet_creation_credentials.dart @@ -4,13 +4,19 @@ import 'package:cw_core/wallet_info.dart'; class NanoNewWalletCredentials extends WalletCredentials { NanoNewWalletCredentials({ required String name, + WalletInfo? walletInfo, String? password, DerivationType? derivationType, + this.mnemonic, + String? parentAddress, }) : super( name: name, password: password, - derivationInfo: DerivationInfo(derivationType: derivationType), + walletInfo: walletInfo, + parentAddress: parentAddress, ); + + final String? mnemonic; } class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index 6dbcc68ab..5c073c9c0 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -32,15 +32,17 @@ class NanoWalletService extends WalletService< @override Future create(NanoNewWalletCredentials credentials, {bool? isTestnet}) async { - // nano standard: - String seedKey = NanoSeeds.generateSeed(); - String mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey); - - // should never happen but just in case: - if (credentials.walletInfo!.derivationInfo == null) { - credentials.walletInfo!.derivationInfo = DerivationInfo(derivationType: DerivationType.nano); - } else if (credentials.walletInfo!.derivationInfo!.derivationType == null) { - credentials.walletInfo!.derivationInfo!.derivationType = DerivationType.nano; + final String mnemonic; + switch (credentials.walletInfo?.derivationInfo?.derivationType) { + case DerivationType.nano: + String seedKey = NanoSeeds.generateSeed(); + mnemonic = credentials.mnemonic ?? NanoDerivations.standardSeedToMnemonic(seedKey); + break; + case DerivationType.bip39: + default: + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); + break; } final wallet = NanoWallet( diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index 4efc312f7..514e73314 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -24,7 +24,7 @@ class PolygonWalletService extends EVMChainWalletService { Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - final mnemonic = bip39.generateMnemonic(strength: strength); + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); final wallet = PolygonWallet( walletInfo: credentials.walletInfo!, diff --git a/cw_solana/lib/solana_wallet_creation_credentials.dart b/cw_solana/lib/solana_wallet_creation_credentials.dart index 5b4fa1774..f0901df79 100644 --- a/cw_solana/lib/solana_wallet_creation_credentials.dart +++ b/cw_solana/lib/solana_wallet_creation_credentials.dart @@ -2,8 +2,19 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class SolanaNewWalletCredentials extends WalletCredentials { - SolanaNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) - : super(name: name, walletInfo: walletInfo, password: password); + SolanaNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + String? parentAddress, + this.mnemonic, + }) : super( + name: name, + walletInfo: walletInfo, + password: password, + parentAddress: parentAddress, + ); + final String? mnemonic; } class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index 7461be33b..aff75373e 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -27,7 +27,7 @@ class SolanaWalletService extends WalletService create(SolanaNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - final mnemonic = bip39.generateMnemonic(strength: strength); + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); final wallet = SolanaWallet( walletInfo: credentials.walletInfo!, diff --git a/cw_tron/lib/tron_wallet_creation_credentials.dart b/cw_tron/lib/tron_wallet_creation_credentials.dart index ed5e1c164..5055c8a8d 100644 --- a/cw_tron/lib/tron_wallet_creation_credentials.dart +++ b/cw_tron/lib/tron_wallet_creation_credentials.dart @@ -2,8 +2,20 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class TronNewWalletCredentials extends WalletCredentials { - TronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) - : super(name: name, walletInfo: walletInfo, password: password); + TronNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + this.mnemonic, + String? parentAddress, + }) : super( + name: name, + walletInfo: walletInfo, + password: password, + parentAddress: parentAddress, + ); + + final String? mnemonic; } class TronRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart index dacef439a..200653eb9 100644 --- a/cw_tron/lib/tron_wallet_service.dart +++ b/cw_tron/lib/tron_wallet_service.dart @@ -39,7 +39,7 @@ class TronWalletService extends WalletService< }) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - final mnemonic = bip39.generateMnemonic(strength: strength); + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); final wallet = TronWallet( walletInfo: credentials.walletInfo!, diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 212d1ec1c..39dcd8b80 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,41 +3,10 @@ PODS: - Flutter - MTBBarcodeScanner - SwiftProtobuf - - BigInt (5.2.0) - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift - CryptoSwift (1.8.2) - - cw_haven (0.0.1): - - cw_haven/Boost (= 0.0.1) - - cw_haven/Haven (= 0.0.1) - - cw_haven/OpenSSL (= 0.0.1) - - cw_haven/Sodium (= 0.0.1) - - cw_shared_external - - Flutter - - cw_haven/Boost (0.0.1): - - cw_shared_external - - Flutter - - cw_haven/Haven (0.0.1): - - cw_shared_external - - Flutter - - cw_haven/OpenSSL (0.0.1): - - cw_shared_external - - Flutter - - cw_haven/Sodium (0.0.1): - - cw_shared_external - - Flutter - - cw_shared_external (0.0.1): - - cw_shared_external/Boost (= 0.0.1) - - cw_shared_external/OpenSSL (= 0.0.1) - - cw_shared_external/Sodium (= 0.0.1) - - Flutter - - cw_shared_external/Boost (0.0.1): - - Flutter - - cw_shared_external/OpenSSL (0.0.1): - - Flutter - - cw_shared_external/Sodium (0.0.1): - - Flutter - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -99,8 +68,6 @@ PODS: - Flutter - MTBBarcodeScanner (5.0.11) - OrderedSet (5.0.0) - - package_info (0.0.1): - - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -131,9 +98,6 @@ PODS: - Toast (4.1.1) - uni_links (0.0.1): - Flutter - - UnstoppableDomainsResolution (4.0.0): - - BigInt - - CryptoSwift - url_launcher_ios (0.0.1): - Flutter - wakelock_plus (0.0.1): @@ -145,8 +109,6 @@ DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -158,7 +120,6 @@ DEPENDENCIES: - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - - package_info (from `.symlinks/plugins/package_info/ios`) - 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`) @@ -168,14 +129,12 @@ DEPENDENCIES: - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sp_scanner (from `.symlinks/plugins/sp_scanner/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - workmanager (from `.symlinks/plugins/workmanager/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - BigInt - CryptoSwift - DKImagePickerController - DKPhotoGallery @@ -187,17 +146,12 @@ SPEC REPOS: - SwiftProtobuf - SwiftyGif - Toast - - UnstoppableDomainsResolution EXTERNAL SOURCES: barcode_scan2: :path: ".symlinks/plugins/barcode_scan2/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" - cw_haven: - :path: ".symlinks/plugins/cw_haven/ios" - cw_shared_external: - :path: ".symlinks/plugins/cw_shared_external/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -220,8 +174,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluttertoast/ios" in_app_review: :path: ".symlinks/plugins/in_app_review/ios" - package_info: - :path: ".symlinks/plugins/package_info/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -249,11 +201,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 - BigInt: f668a80089607f521586bbe29513d708491ef2f7 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea - cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a - cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: b22617f40038496deffba44747101255cee005b0 @@ -269,7 +218,6 @@ SPEC CHECKSUMS: in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 @@ -285,11 +233,10 @@ SPEC CHECKSUMS: SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 -PODFILE CHECKSUM: a2fe518be61cdbdc5b0e2da085ab543d556af2d3 +PODFILE CHECKSUM: e448f662d4c41f0c0b1ccbb78afd57dbf895a597 COCOAPODS: 1.15.2 diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 2ca74822a..f09547fae 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -28,10 +28,22 @@ class CWBitcoin extends Bitcoin { name: name, password: password, wif: wif, walletInfo: walletInfo); @override - WalletCredentials createBitcoinNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password, String? passphrase}) => + WalletCredentials createBitcoinNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + String? passphrase, + String? mnemonic, + String? parentAddress, + }) => BitcoinNewWalletCredentials( - name: name, walletInfo: walletInfo, password: password, passphrase: passphrase); + name: name, + walletInfo: walletInfo, + password: password, + passphrase: passphrase, + mnemonic: mnemonic, + parentAddress: parentAddress, + ); @override WalletCredentials createBitcoinHardwareWalletCredentials( diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index 62ec1539b..b74448703 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -16,9 +16,17 @@ class CWBitcoinCash extends BitcoinCash { WalletInfo? walletInfo, String? password, String? passphrase, + String? mnemonic, + String? parentAddress, }) => BitcoinCashNewWalletCredentials( - name: name, walletInfo: walletInfo, password: password, passphrase: passphrase); + name: name, + walletInfo: walletInfo, + password: password, + passphrase: passphrase, + parentAddress: parentAddress, + mnemonic: mnemonic, + ); @override WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( diff --git a/lib/core/new_wallet_arguments.dart b/lib/core/new_wallet_arguments.dart new file mode 100644 index 000000000..2581c57bb --- /dev/null +++ b/lib/core/new_wallet_arguments.dart @@ -0,0 +1,15 @@ +import 'package:cw_core/wallet_type.dart'; + +class NewWalletArguments { + final WalletType type; + final String? mnemonic; + final String? parentAddress; + final bool isChildWallet; + + NewWalletArguments({ + required this.type, + this.parentAddress, + this.mnemonic, + this.isChildWallet = false, + }); +} diff --git a/lib/core/new_wallet_type_arguments.dart b/lib/core/new_wallet_type_arguments.dart new file mode 100644 index 000000000..b43617835 --- /dev/null +++ b/lib/core/new_wallet_type_arguments.dart @@ -0,0 +1,14 @@ +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; + +class NewWalletTypeArguments { + final void Function(BuildContext, WalletType)? onTypeSelected; + final bool isCreate; + final bool isHardwareWallet; + + NewWalletTypeArguments({ + required this.onTypeSelected, + required this.isCreate, + required this.isHardwareWallet, + }); +} diff --git a/lib/di.dart b/lib/di.dart index 8d8c5a368..63b47f5e1 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -12,10 +12,12 @@ import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; +import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; @@ -30,6 +32,8 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; @@ -145,7 +149,9 @@ import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.d import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dart'; import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; +import 'package:cake_wallet/view_model/new_wallet_type_view_model.dart'; import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; @@ -157,7 +163,6 @@ import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -358,13 +363,31 @@ Future setup({ getIt.get(), (WalletType type) => getIt.get(param1: type))); - getIt.registerFactoryParam((type, _) => WalletNewVM( + getIt.registerFactoryParam( + (newWalletArgs, _) => WalletNewVM( getIt.get(), - getIt.get(param1: type), + getIt.get(param1:newWalletArgs.type), _walletInfoSource, - getIt.get(param1: type), + getIt.get(param1: newWalletArgs.type), getIt.get(), - type: type)); + newWalletArguments: newWalletArgs,)); + + + getIt.registerFactory(() => NewWalletTypeViewModel(_walletInfoSource)); + + getIt.registerFactory( + () => WalletManager(_walletInfoSource, getIt.get()), + ); + + getIt.registerFactoryParam( + (type, _) => WalletGroupsDisplayViewModel( + getIt.get(), + getIt.get(), + getIt.get(), + getIt.get(), + type: type, + ), + ); getIt.registerFactoryParam((args, closable) { return WalletUnlockPage( @@ -723,6 +746,7 @@ Future setup({ _walletInfoSource, getIt.get(), getIt.get(), + getIt.get(), ), ); } else { @@ -733,6 +757,7 @@ Future setup({ _walletInfoSource, getIt.get(), getIt.get(), + getIt.get(), ), ); } @@ -743,17 +768,28 @@ Future setup({ )); getIt.registerFactoryParam( - (WalletListViewModel walletListViewModel, _) => - WalletEditViewModel(walletListViewModel, getIt.get())); + (WalletListViewModel walletListViewModel, _) => WalletEditViewModel( + walletListViewModel, + getIt.get(), + getIt.get(), + ), + ); + + getIt.registerFactoryParam((arguments, _) { - getIt.registerFactoryParam, void>((args, _) { - final walletListViewModel = args.first as WalletListViewModel; - final editingWallet = args.last as WalletListItem; return WalletEditPage( - walletEditViewModel: getIt.get(param1: walletListViewModel), + pageArguments: WalletEditPageArguments( + walletEditViewModel: getIt.get(param1: arguments.walletListViewModel), authService: getIt.get(), - walletNewVM: getIt.get(param1: editingWallet.type), - editingWallet: editingWallet); + walletNewVM: getIt.get( + param1: NewWalletArguments(type: arguments.editingWallet.type), + ), + editingWallet: arguments.editingWallet, + isWalletGroup: arguments.isWalletGroup, + groupName: arguments.groupName, + parentAddress: arguments.parentAddress, + ), + ); }); getIt.registerFactory(() { @@ -1060,15 +1096,11 @@ Future setup({ transactionDetailsViewModel: getIt.get(param1: transactionInfo))); - getIt.registerFactoryParam?>((param1, additionalParams) { - final isCreate = additionalParams?[0] ?? true; - final isHardwareWallet = additionalParams?[1] ?? false; - + getIt.registerFactoryParam( + (newWalletTypeArguments, _) { return NewWalletTypePage( - onTypeSelected: param1, - isCreate: isCreate, - isHardwareWallet: isHardwareWallet, + newWalletTypeArguments: newWalletTypeArguments, + newWalletTypeViewModel: getIt.get(), ); }); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 714612ff1..0c032a736 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -79,6 +79,7 @@ class PreferencesKey { static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status'; static const moneroSeedType = 'monero_seed_type'; static const bitcoinSeedType = 'bitcoin_seed_type'; + static const nanoSeedType = 'nano_seed_type'; static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const donationLinkWalletName = 'donation_link_wallet_name'; diff --git a/lib/entities/seed_type.dart b/lib/entities/seed_type.dart index f941c8a2d..20600e704 100644 --- a/lib/entities/seed_type.dart +++ b/lib/entities/seed_type.dart @@ -65,3 +65,28 @@ class BitcoinSeedType extends EnumerableItem with Serializable { } } } + +class NanoSeedType extends EnumerableItem with Serializable { + const NanoSeedType(this.type, {required String title, required int raw}) + : super(title: title, raw: raw); + + final DerivationType type; + + static const all = [NanoSeedType.nanoStandard, NanoSeedType.bip39]; + + static const defaultDerivationType = bip39; + + static const nanoStandard = NanoSeedType(DerivationType.nano, raw: 0, title: 'Nano'); + static const bip39 = NanoSeedType(DerivationType.bip39, raw: 1, title: 'BIP39'); + + static NanoSeedType deserialize({required int raw}) { + switch (raw) { + case 0: + return nanoStandard; + case 1: + return bip39; + default: + throw Exception('Unexpected token: $raw for SeedType deserialize'); + } + } +} diff --git a/lib/entities/wallet_edit_page_arguments.dart b/lib/entities/wallet_edit_page_arguments.dart new file mode 100644 index 000000000..260471f7e --- /dev/null +++ b/lib/entities/wallet_edit_page_arguments.dart @@ -0,0 +1,28 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; + +class WalletEditPageArguments { + WalletEditPageArguments({ + required this.editingWallet, + this.isWalletGroup = false, + this.walletListViewModel, + this.groupName = '', + this.parentAddress = '', + this.walletEditViewModel, + this.walletNewVM, + this.authService, + }); + + final WalletListItem editingWallet; + final bool isWalletGroup; + final String groupName; + final String parentAddress; + final WalletListViewModel? walletListViewModel; + + final WalletEditViewModel? walletEditViewModel; + final WalletNewVM? walletNewVM; + final AuthService? authService; +} diff --git a/lib/entities/wallet_group.dart b/lib/entities/wallet_group.dart new file mode 100644 index 000000000..9845aea65 --- /dev/null +++ b/lib/entities/wallet_group.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/wallet_info.dart'; + +class WalletGroup { + WalletGroup(this.parentAddress) : wallets = []; + + /// Main identifier for each group, compulsory. + final String parentAddress; + + /// Child wallets that share the same parent address within this group + List wallets; + + /// Custom name for the group, editable for multi-child wallet groups + String? groupName; + + /// Allows editing of the group name (only for multi-child groups). + void setCustomName(String name) { + if (wallets.length > 1) { + groupName = name; + } + } +} diff --git a/lib/entities/wallet_manager.dart b/lib/entities/wallet_manager.dart new file mode 100644 index 000000000..29c873dae --- /dev/null +++ b/lib/entities/wallet_manager.dart @@ -0,0 +1,110 @@ +import 'package:cake_wallet/entities/wallet_group.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:hive/hive.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class WalletManager { + WalletManager( + this._walletInfoSource, + this._sharedPreferences, + ); + + final Box _walletInfoSource; + final SharedPreferences _sharedPreferences; + + final List walletGroups = []; + + /// Categorize wallets into groups based on their parentAddress. + /// + /// Update the lead wallet for each group and clean up empty groups + /// i.e remove group if there's no lead wallet (i.e, no wallets left) + void updateWalletGroups() { + walletGroups.clear(); + + for (var walletInfo in _walletInfoSource.values) { + final group = _getOrCreateGroup(_resolveParentAddress(walletInfo)); + group.wallets.add(walletInfo); + } + + walletGroups.removeWhere((group) => group.wallets.isEmpty); + + _loadCustomGroupNames(); + } + + /// Function to determine the correct parentAddress for a wallet. + /// + /// If it's a parent wallet (parentAddress is null), + /// use its own address as parentAddress. + String _resolveParentAddress(WalletInfo walletInfo) { + return walletInfo.parentAddress ?? walletInfo.address; + } + + /// Check if a group with the parentAddress already exists, + /// If no group exists, create a new one. + /// + WalletGroup _getOrCreateGroup(String parentAddress) { + return walletGroups.firstWhere( + (group) => group.parentAddress == parentAddress, + orElse: () { + final newGroup = WalletGroup(parentAddress); + walletGroups.add(newGroup); + return newGroup; + }, + ); + } + + /// Add a new wallet and update lead wallet after adding. + void addWallet(WalletInfo walletInfo) { + final group = _getOrCreateGroup(_resolveParentAddress(walletInfo)); + group.wallets.add(walletInfo); + } + + /// Removes a wallet from a group i.e when it's deleted. + /// + /// Update lead wallet after removing, + /// Remove the group if it's empty (i.e., no lead wallet). + void removeWallet(WalletInfo walletInfo) { + final group = _getOrCreateGroup(_resolveParentAddress(walletInfo)); + group.wallets.remove(walletInfo); + + if (group.wallets.isEmpty) { + walletGroups.remove(group); + } + } + + /// Returns all the child wallets within a group. + /// + /// If the group is not found, returns an empty group with no wallets. + List getWalletsInGroup(String parentAddress) { + return walletGroups + .firstWhere( + (group) => group.parentAddress == parentAddress, + orElse: () => WalletGroup(parentAddress), + ) + .wallets; + } + + /// Iterate through all groups and load their custom names from storage + void _loadCustomGroupNames() { + for (var group in walletGroups) { + final groupName = _sharedPreferences.getString('wallet_group_name_${group.parentAddress}'); + if (groupName != null && group.wallets.length > 1) { + group.groupName = groupName; // Restore custom name + } + } + } + + /// Save custom name for a group + void _saveCustomGroupName(String parentAddress, String name) { + _sharedPreferences.setString('wallet_group_name_$parentAddress', name); + } + + // Set custom group name and persist it + void setGroupName(String parentAddress, String name) { + if (parentAddress.isEmpty || name.isEmpty) return; + + final group = walletGroups.firstWhere((group) => group.parentAddress == parentAddress); + group.setCustomName(name); + _saveCustomGroupName(parentAddress, name); // Persist the custom name + } +} diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 4e210b227..7796e1c60 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -10,10 +10,18 @@ class CWEthereum extends Ethereum { @override WalletCredentials createEthereumNewWalletCredentials({ required String name, + String? mnemonic, + String? parentAddress, WalletInfo? walletInfo, String? password, }) => - EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + EVMChainNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + parentAddress: parentAddress, + mnemonic: mnemonic, + ); @override WalletCredentials createEthereumRestoreWalletFromSeedCredentials({ diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index 8cf640d8b..83702a8ce 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -91,12 +91,17 @@ class CWNano extends Nano { @override WalletCredentials createNanoNewWalletCredentials({ required String name, + WalletInfo? walletInfo, String? password, + String? mnemonic, + String? parentAddress, }) => NanoNewWalletCredentials( name: name, password: password, - derivationType: DerivationType.nano, + mnemonic: mnemonic, + parentAddress: parentAddress, + walletInfo: walletInfo, ); @override diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 5bb87ff5b..b76626684 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -8,12 +8,19 @@ class CWPolygon extends Polygon { PolygonWalletService(walletInfoSource, isDirect, client: PolygonClient()); @override - WalletCredentials createPolygonNewWalletCredentials({ - required String name, - WalletInfo? walletInfo, - String? password - }) => - EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + WalletCredentials createPolygonNewWalletCredentials( + {required String name, + String? mnemonic, + String? parentAddress, + WalletInfo? walletInfo, + String? password}) => + EVMChainNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + parentAddress: parentAddress, + ); @override WalletCredentials createPolygonRestoreWalletFromSeedCredentials({ @@ -77,21 +84,21 @@ class CWPolygon extends Polygon { int? feeRate, }) => EVMChainTransactionCredentials( - outputs - .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) - .toList(), - priority: priority as EVMChainTransactionPriority, - currency: currency, - feeRate: feeRate, - ); + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: priority as EVMChainTransactionPriority, + currency: currency, + feeRate: feeRate, + ); Object createPolygonTransactionCredentialsRaw( List outputs, { diff --git a/lib/reactions/bip39_wallet_utils.dart b/lib/reactions/bip39_wallet_utils.dart new file mode 100644 index 000000000..a31fec91f --- /dev/null +++ b/lib/reactions/bip39_wallet_utils.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/wallet_type.dart'; + +bool isBIP39Wallet(WalletType walletType) { + switch (walletType) { + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + case WalletType.tron: + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + case WalletType.nano: + case WalletType.banano: + return true; + case WalletType.monero: + case WalletType.wownero: + case WalletType.haven: + case WalletType.none: + return false; + } +} diff --git a/lib/router.dart b/lib/router.dart index 281d50b69..0f65b4a05 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,11 +1,14 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; +import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_nft_response.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -40,9 +43,11 @@ import 'package:cake_wallet/src/screens/faq/faq_page.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; import 'package:cake_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/wallet_group_display_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/advanced_privacy_settings_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/wallet_group_description_page.dart'; import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; @@ -104,6 +109,7 @@ import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; @@ -134,7 +140,11 @@ Route createRoute(RouteSettings settings) { if (SettingsStoreBase.walletPasswordDirectInput) { if (availableWalletTypes.length == 1) { return createRoute( - RouteSettings(name: Routes.newWallet, arguments: availableWalletTypes.first)); + RouteSettings( + name: Routes.newWallet, + arguments: NewWalletArguments(type: availableWalletTypes.first), + ), + ); } else { return createRoute(RouteSettings(name: Routes.newWalletType)); } @@ -144,8 +154,10 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: (PinCodeState context, dynamic _) { if (availableWalletTypes.length == 1) { - Navigator.of(context.context) - .pushNamed(Routes.newWallet, arguments: availableWalletTypes.first); + Navigator.of(context.context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: availableWalletTypes.first), + ); } else { Navigator.of(context.context).pushNamed(Routes.newWalletType); } @@ -154,17 +166,38 @@ Route createRoute(RouteSettings settings) { case Routes.newWalletType: return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) => - Navigator.of(context).pushNamed(Routes.newWallet, arguments: type))); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) => + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: type), + ), + isCreate: true, + isHardwareWallet: false, + ), + ), + ); + + case Routes.walletGroupsDisplayPage: + final type = settings.arguments as WalletType; + final walletGroupsDisplayVM = getIt.get(param1: type); + + return CupertinoPageRoute(builder: (_) => WalletGroupsDisplayPage(walletGroupsDisplayVM)); case Routes.newWallet: - final type = settings.arguments as WalletType; - final walletNewVM = getIt.get(param1: type); + final args = settings.arguments as NewWalletArguments; + + final walletNewVM = getIt.get(param1: args); final seedSettingsViewModel = getIt.get(); return CupertinoPageRoute( - builder: (_) => NewWalletPage(walletNewVM, seedSettingsViewModel)); + builder: (_) => NewWalletPage( + walletNewVM, + seedSettingsViewModel, + isChildWallet: args.isChildWallet, + ), + ); case Routes.chooseHardwareWalletAccount: final arguments = settings.arguments as List; @@ -185,10 +218,15 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletType: return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) => - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), - param2: [false, false])); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) => + Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), + isCreate: false, + isHardwareWallet: false, + ), + ), + ); case Routes.restoreOptions: if (SettingsStoreBase.walletPasswordDirectInput) { @@ -220,10 +258,15 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: availableWalletTypes.first)); } else { return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) => - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), - param2: [false, false])); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) => + Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), + isCreate: false, + isHardwareWallet: false, + ), + ), + ); } case Routes.restoreWalletFromHardwareWallet: @@ -252,23 +295,35 @@ Route createRoute(RouteSettings settings) { )); } else { return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) { - final arguments = ConnectDevicePageParams( - walletType: type, - onConnectDevice: (BuildContext context, _) => Navigator.of(context) - .pushNamed(Routes.chooseHardwareWalletAccount, arguments: [type]), - ); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) { + final arguments = ConnectDevicePageParams( + walletType: type, + onConnectDevice: (BuildContext context, _) => Navigator.of(context) + .pushNamed(Routes.chooseHardwareWalletAccount, arguments: [type]), + ); - Navigator.of(context).pushNamed(Routes.connectDevices, arguments: arguments); - }, - param2: [false, true])); + Navigator.of(context).pushNamed(Routes.connectDevices, arguments: arguments); + }, + isCreate: false, + isHardwareWallet: true, + ), + ), + ); } case Routes.restoreWalletTypeFromQR: return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) => Navigator.of(context).pop(type))); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) => + Navigator.of(context).pop(type), + isCreate: false, + isHardwareWallet: false, + ), + ), + ); case Routes.seed: return MaterialPageRoute( @@ -341,8 +396,10 @@ Route createRoute(RouteSettings settings) { case Routes.walletEdit: return MaterialPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get(param1: settings.arguments as List)); + fullscreenDialog: true, + builder: (_) => + getIt.get(param1: settings.arguments as WalletEditPageArguments), + ); case Routes.auth: return MaterialPageRoute( @@ -592,12 +649,14 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as Map; final type = args['type'] as WalletType; final isFromRestore = args['isFromRestore'] as bool? ?? false; + final isChildWallet = args['isChildWallet'] as bool? ?? false; final useTestnet = args['useTestnet'] as bool; final toggleTestnet = args['toggleTestnet'] as Function(bool? val); return CupertinoPageRoute( builder: (_) => AdvancedPrivacySettingsPage( isFromRestore: isFromRestore, + isChildWallet: isChildWallet, useTestnet: useTestnet, toggleUseTestnet: toggleTestnet, advancedPrivacySettingsViewModel: @@ -712,6 +771,15 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => ConnectDevicePage(params, getIt.get())); + case Routes.walletGroupDescription: + final walletType = settings.arguments as WalletType; + + return MaterialPageRoute( + builder: (_) => WalletGroupDescriptionPage( + selectedWalletType: walletType, + ), + ); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 9c421cab5..83d90248f 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -8,8 +8,7 @@ class Routes { static const restoreWalletFromSeedKeys = '/restore_wallet_from_seeds_keys'; static const restoreWalletFromHardwareWallet = '/restore/hardware_wallet'; static const restoreWalletTypeFromQR = '/restore_wallet_from_qr_code'; - static const restoreWalletChooseDerivation = - '/restore_wallet_choose_derivation'; + static const restoreWalletChooseDerivation = '/restore_wallet_choose_derivation'; static const chooseHardwareWalletAccount = '/restore/hardware_wallet/accounts'; static const dashboard = '/dashboard'; static const send = '/send'; @@ -99,11 +98,13 @@ class Routes { static const editToken = '/edit_token'; static const manageNodes = '/manage_nodes'; static const managePowNodes = '/manage_pow_nodes'; - static const walletConnectConnectionsListing = - '/wallet-connect-connections-listing'; + static const walletConnectConnectionsListing = '/wallet-connect-connections-listing'; static const nftDetailsPage = '/nft_details_page'; static const importNFTPage = '/import_nft_page'; static const torPage = '/tor_page'; + static const signPage = '/sign_page'; static const connectDevices = '/device/connect'; + static const walletGroupsDisplayPage = '/wallet_groups_display_page'; + static const walletGroupDescription = '/wallet_group_description'; } diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index e70739db9..22cc478b6 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -10,10 +10,18 @@ class CWSolana extends Solana { @override WalletCredentials createSolanaNewWalletCredentials({ required String name, + String? mnemonic, + String? parentAddress, WalletInfo? walletInfo, String? password, }) => - SolanaNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + SolanaNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + parentAddress: parentAddress, + ); @override WalletCredentials createSolanaRestoreWalletFromSeedCredentials({ diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 0dd3458b6..f8cbe9120 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -1,4 +1,6 @@ import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/desktop_dropdown_item.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -219,7 +221,7 @@ class _DesktopWalletSelectionDropDownState extends State S.current.privacy_settings; final bool isFromRestore; + final bool isChildWallet; final bool useTestnet; final Function(bool? val) toggleUseTestnet; @override - Widget body(BuildContext context) => _AdvancedPrivacySettingsBody(isFromRestore, useTestnet, - toggleUseTestnet, advancedPrivacySettingsViewModel, nodeViewModel, seedSettingsViewModel); + Widget body(BuildContext context) => _AdvancedPrivacySettingsBody( + isFromRestore, + isChildWallet, + useTestnet, + toggleUseTestnet, + advancedPrivacySettingsViewModel, + nodeViewModel, + seedSettingsViewModel, + ); } class _AdvancedPrivacySettingsBody extends StatefulWidget { - const _AdvancedPrivacySettingsBody(this.isFromRestore, this.useTestnet, this.toggleUseTestnet, - this.privacySettingsViewModel, this.nodeViewModel, this.seedTypeViewModel, - {Key? key}) - : super(key: key); + const _AdvancedPrivacySettingsBody( + this.isFromRestore, + this.isChildWallet, + this.useTestnet, + this.toggleUseTestnet, + this.privacySettingsViewModel, + this.nodeViewModel, + this.seedTypeViewModel, { + Key? key, + }) : super(key: key); final AdvancedPrivacySettingsViewModel privacySettingsViewModel; final NodeCreateOrEditViewModel nodeViewModel; final SeedSettingsViewModel seedTypeViewModel; final bool isFromRestore; + final bool isChildWallet; final bool useTestnet; final Function(bool? val) toggleUseTestnet; @@ -78,6 +96,16 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo passphraseController .addListener(() => widget.seedTypeViewModel.setPassphrase(passphraseController.text)); + + if (widget.isChildWallet) { + if (widget.privacySettingsViewModel.type == WalletType.bitcoin) { + widget.seedTypeViewModel.setBitcoinSeedType(BitcoinSeedType.bip39); + } + + if (widget.privacySettingsViewModel.type == WalletType.nano) { + widget.seedTypeViewModel.setNanoSeedType(NanoSeedType.bip39); + } + } super.initState(); } @@ -116,7 +144,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ), ); }), - if (widget.privacySettingsViewModel.hasSeedTypeOption) + if (widget.privacySettingsViewModel.isMoneroSeedTypeOptionsEnabled) Observer(builder: (_) { return SettingsChoicesCell( ChoicesListItem( @@ -127,15 +155,37 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ), ); }), - if ([WalletType.bitcoin, WalletType.litecoin] - .contains(widget.privacySettingsViewModel.type)) + if (widget.privacySettingsViewModel.isBitcoinSeedTypeOptionsEnabled) Observer(builder: (_) { return SettingsChoicesCell( ChoicesListItem( title: S.current.seedtype, items: BitcoinSeedType.all, selectedItem: widget.seedTypeViewModel.bitcoinSeedType, - onItemSelected: widget.seedTypeViewModel.setBitcoinSeedType, + onItemSelected: (type) { + if (widget.isChildWallet && type != BitcoinSeedType.bip39) { + showAlertForSelectingNonBIP39DerivationTypeForChildWallets(); + } else { + widget.seedTypeViewModel.setBitcoinSeedType(type); + } + }, + ), + ); + }), + if (widget.privacySettingsViewModel.isNanoSeedTypeOptionsEnabled) + Observer(builder: (_) { + return SettingsChoicesCell( + ChoicesListItem( + title: S.current.seedtype, + items: NanoSeedType.all, + selectedItem: widget.seedTypeViewModel.nanoSeedType, + onItemSelected: (type) { + if (widget.isChildWallet && type != NanoSeedType.bip39) { + showAlertForSelectingNonBIP39DerivationTypeForChildWallets(); + } else { + widget.seedTypeViewModel.setNanoSeedType(type); + } + }, ), ); }), @@ -256,6 +306,19 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ); } + void showAlertForSelectingNonBIP39DerivationTypeForChildWallets() { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.seedtype_alert_title, + alertContent: S.current.seedtype_alert_content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }); + } + @override void dispose() { passphraseController diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 8f61ebb38..929e3027a 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -26,10 +26,15 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; class NewWalletPage extends BasePage { - NewWalletPage(this._walletNewVM, this._seedSettingsViewModel); + NewWalletPage( + this._walletNewVM, + this._seedSettingsViewModel, { + this.isChildWallet = false, + }); final WalletNewVM _walletNewVM; final SeedSettingsViewModel _seedSettingsViewModel; + final bool isChildWallet; final walletNameImage = Image.asset('assets/images/wallet_name.png'); @@ -48,15 +53,23 @@ class NewWalletPage extends BasePage { @override Widget body(BuildContext context) => WalletNameForm( - _walletNewVM, - currentTheme.type == ThemeType.dark ? walletNameImage : walletNameLightImage, - _seedSettingsViewModel); + _walletNewVM, + currentTheme.type == ThemeType.dark ? walletNameImage : walletNameLightImage, + _seedSettingsViewModel, + isChildWallet, + ); } class WalletNameForm extends StatefulWidget { - WalletNameForm(this._walletNewVM, this.walletImage, this._seedSettingsViewModel); + WalletNameForm( + this._walletNewVM, + this.walletImage, + this._seedSettingsViewModel, + this.isChildWallet, + ); final WalletNewVM _walletNewVM; + final bool isChildWallet; final Image walletImage; final SeedSettingsViewModel _seedSettingsViewModel; @@ -338,7 +351,8 @@ class _WalletNameFormState extends State { Navigator.of(context).pushNamed(Routes.advancedPrivacySettings, arguments: { "type": _walletNewVM.type, "useTestnet": _walletNewVM.useTestnet, - "toggleTestnet": _walletNewVM.toggleUseTestnet + "toggleTestnet": _walletNewVM.toggleUseTestnet, + "isChildWallet": widget.isChildWallet, }); }, child: Text(S.of(context).advanced_settings), diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 65c7bd59b..faef4b479 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -1,6 +1,10 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'dart:io'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/reactions/bip39_wallet_utils.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart'; @@ -11,6 +15,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_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/new_wallet_type_view_model.dart'; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_type.dart'; @@ -18,21 +23,20 @@ import 'package:flutter/material.dart'; class NewWalletTypePage extends BasePage { NewWalletTypePage({ - required this.onTypeSelected, - required this.isCreate, - required this.isHardwareWallet, + required this.newWalletTypeViewModel, + required this.newWalletTypeArguments, }); - final void Function(BuildContext, WalletType) onTypeSelected; - final bool isCreate; - final bool isHardwareWallet; + final NewWalletTypeViewModel newWalletTypeViewModel; + final NewWalletTypeArguments newWalletTypeArguments; final walletTypeImage = Image.asset('assets/images/wallet_type.png'); final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); @override - String get title => - isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet; + String get title => newWalletTypeArguments.isCreate + ? S.current.wallet_list_create_new_wallet + : S.current.wallet_list_restore_wallet; @override Function(BuildContext)? get pushToNextWidget => (context) { @@ -44,24 +48,27 @@ class NewWalletTypePage extends BasePage { @override Widget body(BuildContext context) => WalletTypeForm( - onTypeSelected: onTypeSelected, walletImage: currentTheme.type == ThemeType.dark ? walletTypeImage : walletTypeLightImage, - isCreate: isCreate, - isHardwareWallet: isHardwareWallet, + isCreate: newWalletTypeArguments.isCreate, + newWalletTypeViewModel: newWalletTypeViewModel, + onTypeSelected: newWalletTypeArguments.onTypeSelected, + isHardwareWallet: newWalletTypeArguments.isHardwareWallet, ); } class WalletTypeForm extends StatefulWidget { WalletTypeForm({ - required this.onTypeSelected, required this.walletImage, required this.isCreate, + required this.newWalletTypeViewModel, + this.onTypeSelected, required this.isHardwareWallet, }); - final void Function(BuildContext, WalletType) onTypeSelected; - final Image walletImage; final bool isCreate; + final Image walletImage; + final NewWalletTypeViewModel newWalletTypeViewModel; + final void Function(BuildContext, WalletType)? onTypeSelected; final bool isHardwareWallet; @override @@ -179,6 +186,18 @@ class WalletTypeFormState extends State { ); } - widget.onTypeSelected(context, selected!); + // If it's a restore flow, trigger the external callback + // If it's not a BIP39 Wallet or if there are no other wallets, route to the newWallet page + // Any other scenario, route to pre-existing seed page + if (!widget.isCreate) { + widget.onTypeSelected!(context, selected!); + } else if (!isBIP39Wallet(selected!) || !widget.newWalletTypeViewModel.hasExisitingWallet) { + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: selected!), + ); + } else { + Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected!); + } } } diff --git a/lib/src/screens/new_wallet/wallet_group_description_page.dart b/lib/src/screens/new_wallet/wallet_group_description_page.dart new file mode 100644 index 000000000..eb3501bed --- /dev/null +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -0,0 +1,90 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; + +class WalletGroupDescriptionPage extends BasePage { + WalletGroupDescriptionPage({required this.selectedWalletType}); + + final WalletType selectedWalletType; + + @override + String get title => S.current.wallet_group; + + @override + Widget body(BuildContext context) { + return Container( + alignment: Alignment.center, + padding: EdgeInsets.all(24), + child: Column( + children: [ + Image.asset( + 'assets/images/wallet_group.png', + scale: 0.8, + ), + SizedBox(height: 32), + Expanded( + child: Text.rich( + TextSpan( + children: [ + TextSpan(text: '${S.of(context).wallet_group_description_one} '), + TextSpan( + text: '${S.of(context).wallet_group.toLowerCase()} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan( + text: '${S.of(context).wallet_group_description_two} ', + ), + TextSpan( + text: '${S.of(context).choose_wallet_group} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan( + text: '${S.of(context).wallet_group_description_three} ', + ), + TextSpan( + text: '${S.of(context).create_new_seed} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan(text: S.of(context).wallet_group_description_four), + ], + ), + textAlign: TextAlign.center, + style: TextStyle( + height: 1.5, + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + ), + PrimaryButton( + onPressed: () => Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: selectedWalletType), + ), + text: S.of(context).create_new_seed, + color: Theme.of(context).cardColor, + textColor: Theme.of(context).extension()!.titleColor, + ), + SizedBox(height: 12), + PrimaryButton( + onPressed: () => Navigator.of(context).pushNamed( + Routes.walletGroupsDisplayPage, + arguments: selectedWalletType, + ), + text: S.of(context).choose_wallet_group, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + SizedBox(height: 32), + ], + ), + ); + } +} diff --git a/lib/src/screens/new_wallet/wallet_group_display_page.dart b/lib/src/screens/new_wallet/wallet_group_display_page.dart new file mode 100644 index 000000000..67dde506e --- /dev/null +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -0,0 +1,192 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +import '../../../themes/extensions/cake_text_theme.dart'; + +class WalletGroupsDisplayPage extends BasePage { + WalletGroupsDisplayPage(this.walletGroupsDisplayViewModel); + + final WalletGroupsDisplayViewModel walletGroupsDisplayViewModel; + + final walletTypeImage = Image.asset('assets/images/wallet_type.png'); + final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); + + @override + String get title => S.current.wallet_group; + + @override + Widget body(BuildContext context) => WalletGroupsDisplayBody( + walletGroupsDisplayViewModel: walletGroupsDisplayViewModel, + ); +} + +class WalletGroupsDisplayBody extends StatelessWidget { + WalletGroupsDisplayBody({required this.walletGroupsDisplayViewModel}); + + final WalletGroupsDisplayViewModel walletGroupsDisplayViewModel; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: EdgeInsets.all(24), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Observer( + builder: (context) { + return Column( + children: [ + if (walletGroupsDisplayViewModel.hasNoFilteredWallet) ...{ + WalletGroupEmptyStateWidget(), + }, + ...walletGroupsDisplayViewModel.multiWalletGroups.map( + (walletGroup) { + return Observer(builder: (context) { + final index = walletGroupsDisplayViewModel.multiWalletGroups + .indexOf(walletGroup); + final group = walletGroupsDisplayViewModel.multiWalletGroups[index]; + final groupName = + group.groupName ?? '${S.of(context).wallet_group} ${index + 1}'; + return GroupedWalletExpansionTile( + leadingWidget: + Icon(Icons.account_balance_wallet_outlined, size: 28), + borderRadius: BorderRadius.all(Radius.circular(16)), + title: groupName, + childWallets: group.wallets.map((walletInfo) { + return walletGroupsDisplayViewModel + .convertWalletInfoToWalletListItem(walletInfo); + }).toList(), + isSelected: + walletGroupsDisplayViewModel.selectedWalletGroup == group, + onTitleTapped: () => + walletGroupsDisplayViewModel.selectWalletGroup(group), + onChildItemTapped: (_) => + walletGroupsDisplayViewModel.selectWalletGroup(group), + ); + }); + }, + ).toList(), + ...walletGroupsDisplayViewModel.singleWalletsList.map((singleWallet) { + return Observer( + builder: (context) { + final index = walletGroupsDisplayViewModel.singleWalletsList + .indexOf(singleWallet); + final wallet = walletGroupsDisplayViewModel.singleWalletsList[index]; + return GroupedWalletExpansionTile( + borderRadius: BorderRadius.all(Radius.circular(16)), + title: wallet.name, + isSelected: + walletGroupsDisplayViewModel.selectedSingleWallet == wallet, + leadingWidget: Image.asset( + walletTypeToCryptoCurrency(wallet.type).iconPath!, + width: 32, + height: 32, + ), + onTitleTapped: () => + walletGroupsDisplayViewModel.selectSingleWallet(wallet), + ); + }, + ); + }).toList(), + ], + ); + }, + ), + ), + ), + Observer( + builder: (context) { + return LoadingPrimaryButton( + isLoading: walletGroupsDisplayViewModel.isFetchingMnemonic, + onPressed: () { + if (walletGroupsDisplayViewModel.hasNoFilteredWallet) { + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: walletGroupsDisplayViewModel.type), + ); + } else { + onTypeSelected(context); + } + }, + text: walletGroupsDisplayViewModel.hasNoFilteredWallet + ? S.of(context).create_new_seed + : S.of(context).seed_language_next, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isDisabled: !walletGroupsDisplayViewModel.hasNoFilteredWallet + ? (walletGroupsDisplayViewModel.selectedWalletGroup == null && + walletGroupsDisplayViewModel.selectedSingleWallet == null) + : false, + ); + }, + ), + SizedBox(height: 32), + ], + ), + ), + ); + } + + Future onTypeSelected(BuildContext context) async { + final mnemonic = await walletGroupsDisplayViewModel.getSelectedWalletMnemonic(); + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments( + type: walletGroupsDisplayViewModel.type, + mnemonic: mnemonic, + parentAddress: walletGroupsDisplayViewModel.parentAddress, + isChildWallet: true, + ), + ); + } +} + +class WalletGroupEmptyStateWidget extends StatelessWidget { + const WalletGroupEmptyStateWidget({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Image.asset( + 'assets/images/wallet_group.png', + scale: 0.8, + ), + SizedBox(height: 32), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${S.of(context).wallet_group_empty_state_text_one} ', + ), + TextSpan( + text: '${S.of(context).create_new_seed} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan(text: S.of(context).wallet_group_empty_state_text_two), + ], + ), + textAlign: TextAlign.center, + style: TextStyle( + height: 1.5, + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + ], + ); + } +} diff --git a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart new file mode 100644 index 000000000..52e2e74b0 --- /dev/null +++ b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart @@ -0,0 +1,119 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; + +class GroupedWalletExpansionTile extends StatelessWidget { + GroupedWalletExpansionTile({ + required this.title, + required this.isSelected, + this.childWallets = const [], + this.onTitleTapped, + this.onChildItemTapped = _defaultVoidCallback, + this.leadingWidget, + this.trailingWidget, + this.childTrailingWidget, + this.decoration, + this.color, + this.textColor, + this.arrowColor, + this.borderRadius, + this.margin, + this.tileKey, + }) : super(key: tileKey); + + final Key? tileKey; + final bool isSelected; + + final VoidCallback? onTitleTapped; + final void Function(WalletListItem item) onChildItemTapped; + + final String title; + final Widget? leadingWidget; + final Widget? trailingWidget; + final Widget Function(WalletListItem)? childTrailingWidget; + + final List childWallets; + + final Color? color; + final Color? textColor; + final Color? arrowColor; + final EdgeInsets? margin; + final Decoration? decoration; + final BorderRadius? borderRadius; + + static void _defaultVoidCallback(WalletListItem ITEM) {} + + @override + Widget build(BuildContext context) { + final backgroundColor = color ?? (isSelected ? Colors.green : Theme.of(context).cardColor); + final effectiveTextColor = textColor ?? + (isSelected + ? Theme.of(context).extension()!.restoreWalletButtonTextColor + : Theme.of(context).extension()!.buttonTextColor); + + final effectiveArrowColor = arrowColor ?? + (isSelected + ? Theme.of(context).extension()!.restoreWalletButtonTextColor + : Theme.of(context).extension()!.titlesColor); + return Container( + decoration: BoxDecoration( + borderRadius: borderRadius ?? BorderRadius.all(Radius.circular(30)), + color: backgroundColor, + ), + margin: margin ?? const EdgeInsets.only(bottom: 12.0), + child: Theme( + data: Theme.of(context).copyWith( + dividerColor: Colors.transparent, + splashFactory: NoSplash.splashFactory, + ), + child: ExpansionTile( + key: tileKey, + tilePadding: EdgeInsets.symmetric(vertical: 1, horizontal: 16), + iconColor: effectiveArrowColor, + collapsedIconColor: effectiveArrowColor, + leading: leadingWidget, + trailing: trailingWidget ?? (childWallets.isEmpty ? SizedBox.shrink() : null), + title: GestureDetector( + onTap: onTitleTapped, + child: Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: effectiveTextColor, + ), + textAlign: TextAlign.left, + ), + ), + children: childWallets.map( + (item) { + final walletTypeToCrypto = walletTypeToCryptoCurrency(item.type); + return ListTile( + key: ValueKey(item.name), + trailing: childTrailingWidget?.call(item), + onTap: () => onChildItemTapped(item), + leading: Image.asset( + walletTypeToCrypto.iconPath!, + width: 32, + height: 32, + ), + title: Text( + item.name, + maxLines: 1, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: effectiveTextColor, + ), + ), + ); + }, + ).toList(), + ), + ), + ); + } +} diff --git a/lib/src/screens/new_wallet/widgets/select_button.dart b/lib/src/screens/new_wallet/widgets/select_button.dart index 2eda77d01..834d85986 100644 --- a/lib/src/screens/new_wallet/widgets/select_button.dart +++ b/lib/src/screens/new_wallet/widgets/select_button.dart @@ -18,9 +18,11 @@ class SelectButton extends StatelessWidget { this.arrowColor, this.borderColor, this.deviceConnectionTypes, + this.borderRadius, + this.padding, }); - final Image? image; + final Widget? image; final String text; final double textSize; final bool isSelected; @@ -32,6 +34,8 @@ class SelectButton extends StatelessWidget { final Color? textColor; final Color? arrowColor; final Color? borderColor; + final BorderRadius? borderRadius; + final EdgeInsets? padding; @override Widget build(BuildContext context) { @@ -62,10 +66,10 @@ class SelectButton extends StatelessWidget { child: Container( width: double.infinity, height: height, - padding: EdgeInsets.only(left: 30, right: 30), + padding: padding ?? EdgeInsets.only(left: 30, right: 30), alignment: Alignment.center, decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(30)), + borderRadius: borderRadius ?? BorderRadius.all(Radius.circular(30)), color: backgroundColor, border: borderColor != null ? Border.all(color: borderColor!) : null, ), diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 7d7fdb889..a855088e4 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -281,16 +281,9 @@ class WalletRestorePage extends BasePage { return false; } - if ((walletRestoreViewModel.type == WalletType.litecoin) && - (seedWords.length != WalletRestoreViewModelBase.electrumSeedMnemonicLength && - seedWords.length != WalletRestoreViewModelBase.electrumShortSeedMnemonicLength)) { - return false; - } - // bip39: const validSeedLengths = [12, 18, 24]; - if (walletRestoreViewModel.type == WalletType.bitcoin && - !(validSeedLengths.contains(seedWords.length))) { + if (!(validSeedLengths.contains(seedWords.length))) { return false; } diff --git a/lib/src/screens/wallet/wallet_edit_page.dart b/lib/src/screens/wallet/wallet_edit_page.dart index 2d1bb9e47..9515e58c3 100644 --- a/lib/src/screens/wallet/wallet_edit_page.dart +++ b/lib/src/screens/wallet/wallet_edit_page.dart @@ -1,20 +1,16 @@ import 'package:another_flushbar/flushbar.dart'; -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/wallet_name_validator.dart'; +import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; -import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; @@ -22,29 +18,29 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; + class WalletEditPage extends BasePage { - WalletEditPage( - {required this.walletEditViewModel, - required this.editingWallet, - required this.walletNewVM, - required this.authService}) - : _formKey = GlobalKey(), + WalletEditPage({ + required this.pageArguments, + }) : _formKey = GlobalKey(), _labelController = TextEditingController(), + walletEditViewModel = pageArguments.walletEditViewModel!, super() { - _labelController.text = editingWallet.name; + _labelController.text = + pageArguments.isWalletGroup ? pageArguments.groupName : pageArguments.editingWallet.name; _labelController.addListener(() => walletEditViewModel.newName = _labelController.text); } final GlobalKey _formKey; final TextEditingController _labelController; + final WalletEditPageArguments pageArguments; final WalletEditViewModel walletEditViewModel; - final WalletNewVM walletNewVM; - final WalletListItem editingWallet; - final AuthService authService; @override - String get title => S.current.wallet_list_edit_wallet; + String get title => pageArguments.isWalletGroup + ? S.current.wallet_list_edit_group_name + : S.current.wallet_list_edit_wallet; Flushbar? _progressBar; @@ -57,11 +53,14 @@ class WalletEditPage extends BasePage { child: Column( children: [ Expanded( - child: Center( - child: BaseTextFormField( - controller: _labelController, - hintText: S.of(context).wallet_list_wallet_name, - validator: WalletNameValidator()))), + child: Center( + child: BaseTextFormField( + controller: _labelController, + hintText: S.of(context).wallet_list_wallet_name, + validator: WalletNameValidator(), + ), + ), + ), Observer( builder: (_) { final isLoading = walletEditViewModel.state is WalletEditRenamePending || @@ -69,24 +68,26 @@ class WalletEditPage extends BasePage { return Row( children: [ - Flexible( - child: Container( - padding: EdgeInsets.only(right: 8.0), - child: LoadingPrimaryButton( - isDisabled: isLoading, - onPressed: () => _removeWallet(context), - text: S.of(context).delete, - color: Palette.red, - textColor: Colors.white), + if (!pageArguments.isWalletGroup) + Flexible( + child: Container( + padding: EdgeInsets.only(right: 8.0), + child: LoadingPrimaryButton( + isDisabled: isLoading, + onPressed: () => _removeWallet(context), + text: S.of(context).delete, + color: Palette.red, + textColor: Colors.white), + ), ), - ), Flexible( child: Container( padding: EdgeInsets.only(left: 8.0), child: LoadingPrimaryButton( onPressed: () async { if (_formKey.currentState?.validate() ?? false) { - if (walletNewVM.nameExists(walletEditViewModel.newName)) { + if (pageArguments.walletNewVM! + .nameExists(walletEditViewModel.newName)) { showPopUp( context: context, builder: (_) { @@ -102,29 +103,33 @@ class WalletEditPage extends BasePage { try { bool confirmed = false; - if (SettingsStoreBase - .walletPasswordDirectInput) { + if (SettingsStoreBase.walletPasswordDirectInput) { await Navigator.of(context).pushNamed( Routes.walletUnlockLoadable, arguments: WalletUnlockArguments( - authPasswordHandler: - (String password) async { - await walletEditViewModel - .changeName(editingWallet, - password: password); + authPasswordHandler: (String password) async { + await walletEditViewModel.changeName( + pageArguments.editingWallet, + password: password, + isWalletGroup: pageArguments.isWalletGroup, + groupParentAddress: pageArguments.parentAddress, + ); }, - callback: (bool - isAuthenticatedSuccessfully, + callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { if (isAuthenticatedSuccessfully) { auth.close(); confirmed = true; } }, - walletName: editingWallet.name, - walletType: editingWallet.type)); + walletName: pageArguments.editingWallet.name, + walletType: pageArguments.editingWallet.type)); } else { - await walletEditViewModel.changeName(editingWallet); + await walletEditViewModel.changeName( + pageArguments.editingWallet, + isWalletGroup: pageArguments.isWalletGroup, + groupParentAddress: pageArguments.parentAddress, + ); confirmed = true; } @@ -154,7 +159,9 @@ class WalletEditPage extends BasePage { } Future _removeWallet(BuildContext context) async { - authService.authenticateAction(context, onAuthSuccess: (isAuthenticatedSuccessfully) async { + pageArguments.authService!.authenticateAction( + context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { if (!isAuthenticatedSuccessfully) { return; } @@ -173,7 +180,8 @@ class WalletEditPage extends BasePage { builder: (BuildContext dialogContext) { return AlertWithTwoActions( alertTitle: S.of(context).delete_wallet, - alertContent: S.of(context).delete_wallet_confirm_message(editingWallet.name), + alertContent: + S.of(context).delete_wallet_confirm_message(pageArguments.editingWallet.name), leftButtonText: S.of(context).cancel, rightButtonText: S.of(context).delete, actionLeftButton: () => Navigator.of(dialogContext).pop(), @@ -187,13 +195,16 @@ class WalletEditPage extends BasePage { Navigator.of(context).pop(); try { - changeProcessText(context, S.of(context).wallet_list_removing_wallet(editingWallet.name)); - await walletEditViewModel.remove(editingWallet); + changeProcessText( + context, S.of(context).wallet_list_removing_wallet(pageArguments.editingWallet.name)); + await walletEditViewModel.remove(pageArguments.editingWallet); hideProgressText(); } catch (e) { changeProcessText( context, - S.of(context).wallet_list_failed_to_remove(editingWallet.name, e.toString()), + S + .of(context) + .wallet_list_failed_to_remove(pageArguments.editingWallet.name, e.toString()), ); } } diff --git a/lib/src/screens/wallet_list/edit_wallet_button_widget.dart b/lib/src/screens/wallet_list/edit_wallet_button_widget.dart new file mode 100644 index 000000000..8f858348a --- /dev/null +++ b/lib/src/screens/wallet_list/edit_wallet_button_widget.dart @@ -0,0 +1,54 @@ +import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:flutter/material.dart'; + +class EditWalletButtonWidget extends StatelessWidget { + const EditWalletButtonWidget({ + required this.width, + required this.onTap, + this.isGroup = false, + super.key, + }); + + final bool isGroup; + final double width; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Container( + width: width, + child: Row( + children: [ + GestureDetector( + onTap: onTap, + child: Center( + child: Container( + height: 40, + width: 44, + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).extension()!.iconsBackgroundColor, + ), + child: Icon( + Icons.edit, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + ), + ), + ), + ), + if (isGroup) ...{ + SizedBox(width: 6), + Icon( + Icons.keyboard_arrow_down, + size: 24, + color: Theme.of(context).extension()!.titlesColor, + ), + }, + ], + ), + ); + } +} diff --git a/lib/src/screens/wallet_list/filtered_list.dart b/lib/src/screens/wallet_list/filtered_list.dart index 7149833a8..63a1ae392 100644 --- a/lib/src/screens/wallet_list/filtered_list.dart +++ b/lib/src/screens/wallet_list/filtered_list.dart @@ -7,11 +7,13 @@ class FilteredList extends StatefulWidget { required this.list, required this.itemBuilder, required this.updateFunction, + this.shrinkWrap = false, }); final ObservableList list; final Widget Function(BuildContext, int) itemBuilder; final Function updateFunction; + final bool shrinkWrap; @override FilteredListState createState() => FilteredListState(); @@ -22,6 +24,7 @@ class FilteredListState extends State { Widget build(BuildContext context) { return Observer( builder: (_) => ReorderableListView.builder( + shrinkWrap: widget.shrinkWrap, physics: const BouncingScrollPhysics(), itemBuilder: widget.itemBuilder, itemCount: widget.list.length, diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 0d6d9e912..9c210d8eb 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -1,5 +1,9 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart'; +import 'package:cake_wallet/src/screens/wallet_list/edit_wallet_button_widget.dart'; import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -7,8 +11,6 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; -import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -23,7 +25,6 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; class WalletListPage extends BasePage { WalletListPage({required this.walletListViewModel, required this.authService}); @@ -128,112 +129,143 @@ class WalletListBodyState extends State { child: Column( children: [ Expanded( - child: Container( - child: Observer( - builder: (_) => FilteredList( - list: widget.walletListViewModel.wallets, - updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, - itemBuilder: (__, index) { - final wallet = widget.walletListViewModel.wallets[index]; - final currentColor = wallet.isCurrent - ? Theme.of(context) - .extension()! - .createNewWalletButtonBackgroundColor - : Theme.of(context).colorScheme.background; - final row = GestureDetector( - key: ValueKey(wallet.name), - onTap: () => wallet.isCurrent ? null : _loadWallet(wallet), - child: Container( - height: tileHeight, - width: double.infinity, - child: Row( - children: [ - Container( - height: tileHeight, - width: 4, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(4), - bottomRight: Radius.circular(4)), - color: currentColor), - ), - Expanded( - child: Container( - height: tileHeight, - padding: EdgeInsets.only(left: 20, right: 20), - color: Theme.of(context).colorScheme.background, - alignment: Alignment.centerLeft, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - wallet.isEnabled - ? _imageFor( - type: wallet.type, - isTestnet: wallet.isTestnet, - ) - : nonWalletTypeIcon, - SizedBox(width: 10), - Flexible( - child: Text( - wallet.name, - maxLines: null, - softWrap: true, - style: TextStyle( - fontSize: DeviceInfo.instance.isDesktop ? 18 : 20, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .extension()! - .titleColor, - ), - ), - ), - ], - ), - ), - ), - ], + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.walletListViewModel.multiWalletGroups.isNotEmpty) ...{ + Padding( + padding: const EdgeInsets.only(left: 24), + child: Text( + S.current.shared_seed_wallet_groups, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, ), ), - ); - - return wallet.isCurrent - ? row - : Row( - key: ValueKey(wallet.name), - children: [ - Expanded(child: row), - GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.walletEdit, - arguments: [widget.walletListViewModel, wallet]), - child: Container( - padding: EdgeInsets.only( - right: DeviceInfo.instance.isMobile ? 20 : 40), - child: Center( - child: Container( - height: 40, - width: 44, - padding: EdgeInsets.all(10), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context) - .extension()! - .iconsBackgroundColor, - ), - child: Icon( - Icons.edit, - size: 14, - color: Theme.of(context) - .extension()! - .iconsColor, - ), - ), - ), - ), + ), + SizedBox(height: 16), + Container( + child: Observer( + builder: (_) => FilteredList( + shrinkWrap: true, + list: widget.walletListViewModel.multiWalletGroups, + updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, + itemBuilder: (context, index) { + final group = widget.walletListViewModel.multiWalletGroups[index]; + final groupName = group.groupName ?? + '${S.current.wallet_group} ${index + 1}'; + return GroupedWalletExpansionTile( + borderRadius: BorderRadius.all(Radius.circular(16)), + margin: EdgeInsets.only(left: 20, right: 20, bottom: 12), + title: groupName, + tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), + leadingWidget: Icon( + Icons.account_balance_wallet_outlined, + size: 28, ), - ], - ); + trailingWidget: EditWalletButtonWidget( + width: 74, + isGroup: true, + onTap: () { + final wallet = widget.walletListViewModel + .convertWalletInfoToWalletListItem(group.wallets.first); + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: wallet, + isWalletGroup: true, + groupName: groupName, + parentAddress: group.parentAddress, + ), + ); + }, + ), + childWallets: group.wallets.map((walletInfo) { + return widget.walletListViewModel + .convertWalletInfoToWalletListItem(walletInfo); + }).toList(), + isSelected: false, + onChildItemTapped: (wallet) => + wallet.isCurrent ? null : _loadWallet(wallet), + childTrailingWidget: (item) { + return item.isCurrent + ? SizedBox.shrink() + : EditWalletButtonWidget( + width: 44, + onTap: () => Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: item, + ), + ), + ); + }, + ); + }, + ), + ), + ), + SizedBox(height: 24), }, - ), + if (widget.walletListViewModel.singleWalletsList.isNotEmpty) ...{ + Padding( + padding: const EdgeInsets.only(left: 24), + child: Text( + S.current.single_seed_wallets_group, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + SizedBox(height: 16), + Container( + child: Observer( + builder: (_) => FilteredList( + shrinkWrap: true, + list: widget.walletListViewModel.singleWalletsList, + updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, + itemBuilder: (context, index) { + final wallet = widget.walletListViewModel.singleWalletsList[index]; + + return GroupedWalletExpansionTile( + tileKey: ValueKey('single_wallets_expansion_tile_widget_$index'), + leadingWidget: Image.asset( + walletTypeToCryptoCurrency(wallet.type).iconPath!, + width: 32, + height: 32, + ), + title: wallet.name, + isSelected: false, + borderRadius: BorderRadius.all(Radius.circular(16)), + margin: EdgeInsets.only(left: 20, right: 20, bottom: 12), + onTitleTapped: () => wallet.isCurrent ? null : _loadWallet(wallet), + trailingWidget: wallet.isCurrent + ? null + : EditWalletButtonWidget( + width: 44, + onTap: () { + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: wallet, + ), + ); + }, + ), + ); + }, + ), + ), + ), + }, + ], ), ), ), @@ -249,14 +281,18 @@ class WalletListBodyState extends State { widget.authService.authenticateAction( context, route: Routes.newWallet, - arguments: widget.walletListViewModel.currentWalletType, + arguments: NewWalletArguments( + type: widget.walletListViewModel.currentWalletType, + ), conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, ); } else { Navigator.of(context).pushNamed( Routes.newWallet, - arguments: widget.walletListViewModel.currentWalletType, + arguments: NewWalletArguments( + type: widget.walletListViewModel.currentWalletType, + ), ); } } else { @@ -340,15 +376,15 @@ class WalletListBodyState extends State { Future _loadWallet(WalletListItem wallet) async { if (SettingsStoreBase.walletPasswordDirectInput) { - Navigator.of(context).pushNamed( - Routes.walletUnlockLoadable, + Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, arguments: WalletUnlockArguments( callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { if (isAuthenticatedSuccessfully) { auth.close(); setState(() {}); } - }, walletName: wallet.name, + }, + walletName: wallet.name, walletType: wallet.type)); return; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index f2738a293..6873ffde8 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -58,6 +58,7 @@ abstract class SettingsStoreBase with Store { required AutoGenerateSubaddressStatus initialAutoGenerateSubaddressStatus, required MoneroSeedType initialMoneroSeedType, required BitcoinSeedType initialBitcoinSeedType, + required NanoSeedType initialNanoSeedType, required bool initialAppSecure, required bool initialDisableBuy, required bool initialDisableSell, @@ -132,6 +133,7 @@ abstract class SettingsStoreBase with Store { autoGenerateSubaddressStatus = initialAutoGenerateSubaddressStatus, moneroSeedType = initialMoneroSeedType, bitcoinSeedType = initialBitcoinSeedType, + nanoSeedType = initialNanoSeedType, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, selectedCake2FAPreset = initialCake2FAPresetOptions, @@ -341,6 +343,11 @@ abstract class SettingsStoreBase with Store { (BitcoinSeedType bitcoinSeedType) => sharedPreferences.setInt( PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); + reaction( + (_) => nanoSeedType, + (NanoSeedType nanoSeedType) => + sharedPreferences.setInt(PreferencesKey.nanoSeedType, nanoSeedType.raw)); + reaction( (_) => fiatApiMode, (FiatApiMode mode) => @@ -569,6 +576,7 @@ abstract class SettingsStoreBase with Store { static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; static const defaultMoneroSeedType = MoneroSeedType.defaultSeedType; static const defaultBitcoinSeedType = BitcoinSeedType.defaultDerivationType; + static const defaultNanoSeedType = NanoSeedType.defaultDerivationType; @observable FiatCurrency fiatCurrency; @@ -603,6 +611,9 @@ abstract class SettingsStoreBase with Store { @observable BitcoinSeedType bitcoinSeedType; + @observable + NanoSeedType nanoSeedType; + @observable bool isAppSecure; @@ -974,6 +985,11 @@ abstract class SettingsStoreBase with Store { ? BitcoinSeedType.deserialize(raw: _bitcoinSeedType) : defaultBitcoinSeedType; + final _nanoSeedType = sharedPreferences.getInt(PreferencesKey.nanoSeedType); + + final nanoSeedType = + _nanoSeedType != null ? NanoSeedType.deserialize(raw: _nanoSeedType) : defaultNanoSeedType; + final nodes = {}; final powNodes = {}; @@ -1138,6 +1154,7 @@ abstract class SettingsStoreBase with Store { initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, initialMoneroSeedType: moneroSeedType, initialBitcoinSeedType: bitcoinSeedType, + initialNanoSeedType: nanoSeedType, initialAppSecure: isAppSecure, initialDisableBuy: disableBuy, initialDisableSell: disableSell, @@ -1270,6 +1287,11 @@ abstract class SettingsStoreBase with Store { ? BitcoinSeedType.deserialize(raw: _bitcoinSeedType) : defaultBitcoinSeedType; + final _nanoSeedType = sharedPreferences.getInt(PreferencesKey.nanoSeedType); + + nanoSeedType = + _nanoSeedType != null ? NanoSeedType.deserialize(raw: _nanoSeedType) : defaultNanoSeedType; + balanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); shouldSaveRecipientAddress = diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index c6ac89342..b4aed4235 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -13,8 +13,15 @@ class CWTron extends Tron { required String name, WalletInfo? walletInfo, String? password, + String? mnemonic, + String? parentAddress, }) => - TronNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + TronNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + parentAddress: parentAddress); @override WalletCredentials createTronRestoreWalletFromSeedCredentials({ @@ -34,7 +41,7 @@ class CWTron extends Tron { @override String getAddress(WalletBase wallet) => (wallet as TronWallet).walletAddresses.address; - + Object createTronTransactionCredentials( List outputs, { required CryptoCurrency currency, @@ -63,10 +70,10 @@ class CWTron extends Tron { @override Future addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress) async { - final tronToken = TronToken( + final tronToken = TronToken( name: token.name, symbol: token.title, - contractAddress: contractAddress, + contractAddress: contractAddress, decimal: token.decimals, enabled: token.enabled, iconPath: token.iconPath, diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index da294cdea..b97c796f7 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -46,17 +46,30 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.litecoin: return _settingsStore.bitcoinSeedType == BitcoinSeedType.bip39; + case WalletType.nano: + case WalletType.banano: + return _settingsStore.nanoSeedType == NanoSeedType.bip39; + case WalletType.monero: case WalletType.wownero: case WalletType.none: case WalletType.haven: - case WalletType.nano: - case WalletType.banano: return false; } } - bool get hasSeedTypeOption => [WalletType.monero, WalletType.wownero].contains(type); + + bool get isMoneroSeedTypeOptionsEnabled => [ + WalletType.monero, + WalletType.wownero, + ].contains(type); + + bool get isBitcoinSeedTypeOptionsEnabled => [ + WalletType.bitcoin, + WalletType.litecoin, + ].contains(type); + + bool get isNanoSeedTypeOptionsEnabled => [WalletType.nano].contains(type); bool get hasPassphraseOption => [ WalletType.bitcoin, diff --git a/lib/view_model/new_wallet_type_view_model.dart b/lib/view_model/new_wallet_type_view_model.dart new file mode 100644 index 000000000..9237bc692 --- /dev/null +++ b/lib/view_model/new_wallet_type_view_model.dart @@ -0,0 +1,16 @@ +import 'package:cw_core/wallet_info.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; + +part 'new_wallet_type_view_model.g.dart'; + +class NewWalletTypeViewModel = NewWalletTypeViewModelBase with _$NewWalletTypeViewModel; + +abstract class NewWalletTypeViewModelBase with Store { + NewWalletTypeViewModelBase(this._walletInfoSource); + + @computed + bool get hasExisitingWallet => _walletInfoSource.isNotEmpty; + + final Box _walletInfoSource; +} diff --git a/lib/view_model/seed_settings_view_model.dart b/lib/view_model/seed_settings_view_model.dart index 3a9536885..221899676 100644 --- a/lib/view_model/seed_settings_view_model.dart +++ b/lib/view_model/seed_settings_view_model.dart @@ -23,6 +23,13 @@ abstract class SeedSettingsViewModelBase with Store { void setBitcoinSeedType(BitcoinSeedType derivationType) => _appStore.settingsStore.bitcoinSeedType = derivationType; + @computed + NanoSeedType get nanoSeedType => _appStore.settingsStore.nanoSeedType; + + @action + void setNanoSeedType(NanoSeedType derivationType) => + _appStore.settingsStore.nanoSeedType = derivationType; + @computed String? get passphrase => this._seedSettingsStore.passphrase; diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index e563be37a..7041fa382 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -99,6 +99,7 @@ abstract class WalletCreationVMBase with Store { showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven, derivationInfo: credentials.derivationInfo ?? getDefaultCreateDerivation(), hardwareWalletType: credentials.hardwareWalletType, + parentAddress: credentials.parentAddress, ); credentials.walletInfo = walletInfo; @@ -117,12 +118,16 @@ abstract class WalletCreationVMBase with Store { } DerivationInfo? getDefaultCreateDerivation() { - final useBip39 = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.bip39; + final useBip39ForBitcoin = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.bip39; + final useBip39ForNano = seedSettingsViewModel.nanoSeedType.type == DerivationType.bip39; switch (type) { case WalletType.nano: + if (useBip39ForNano) { + return DerivationInfo(derivationType: DerivationType.bip39); + } return DerivationInfo(derivationType: DerivationType.nano); case WalletType.bitcoin: - if (useBip39) { + if (useBip39ForBitcoin) { return DerivationInfo( derivationType: DerivationType.bip39, derivationPath: "m/84'/0'/0'", @@ -132,7 +137,7 @@ abstract class WalletCreationVMBase with Store { } return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; case WalletType.litecoin: - if (useBip39) { + if (useBip39ForBitcoin) { return DerivationInfo( derivationType: DerivationType.bip39, derivationPath: "m/84'/2'/0'", @@ -148,9 +153,13 @@ abstract class WalletCreationVMBase with Store { DerivationInfo? getCommonRestoreDerivation() { final useElectrum = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.electrum; + final useNanoStandard = seedSettingsViewModel.nanoSeedType.type == DerivationType.nano; switch (this.type) { case WalletType.nano: - return DerivationInfo(derivationType: DerivationType.nano); + if (useNanoStandard) { + return DerivationInfo(derivationType: DerivationType.nano); + } + return DerivationInfo(derivationType: DerivationType.bip39); case WalletType.bitcoin: if (useElectrum) { return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; diff --git a/lib/view_model/wallet_groups_display_view_model.dart b/lib/view_model/wallet_groups_display_view_model.dart new file mode 100644 index 000000000..08515febf --- /dev/null +++ b/lib/view_model/wallet_groups_display_view_model.dart @@ -0,0 +1,165 @@ +import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/wallet_group.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; +import 'package:cake_wallet/reactions/bip39_wallet_utils.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:mobx/mobx.dart'; + +part 'wallet_groups_display_view_model.g.dart'; + +class WalletGroupsDisplayViewModel = WalletGroupsDisplayViewModelBase + with _$WalletGroupsDisplayViewModel; + +abstract class WalletGroupsDisplayViewModelBase with Store { + WalletGroupsDisplayViewModelBase( + this._appStore, + this._walletLoadingService, + this._walletManager, + this.walletListViewModel, { + required this.type, + }) : isFetchingMnemonic = false, + multiWalletGroups = ObservableList(), + singleWalletsList = ObservableList() { + reaction((_) => _appStore.wallet, (_) => updateWalletInfoSourceList()); + updateWalletInfoSourceList(); + } + + final WalletType type; + final AppStore _appStore; + final WalletManager _walletManager; + final WalletLoadingService _walletLoadingService; + final WalletListViewModel walletListViewModel; + + @observable + ObservableList multiWalletGroups; + + @observable + ObservableList singleWalletsList; + + @observable + WalletGroup? selectedWalletGroup; + + @observable + WalletInfo? selectedSingleWallet; + + @observable + String? parentAddress; + + @observable + bool isFetchingMnemonic; + + @computed + bool get hasNoFilteredWallet { + return singleWalletsList.isEmpty && multiWalletGroups.isEmpty; + } + + @action + Future getSelectedWalletMnemonic() async { + WalletListItem walletToUse; + + bool isGroupSelected = selectedWalletGroup != null; + + if (isGroupSelected) { + walletToUse = convertWalletInfoToWalletListItem(selectedWalletGroup!.wallets.first); + } else { + walletToUse = convertWalletInfoToWalletListItem(selectedSingleWallet!); + } + + try { + isFetchingMnemonic = true; + final wallet = await _walletLoadingService.load( + walletToUse.type, + walletToUse.name, + ); + + parentAddress = + isGroupSelected ? selectedWalletGroup!.parentAddress : selectedSingleWallet!.address; + + return wallet.seed; + } catch (e) { + return null; + } finally { + isFetchingMnemonic = false; + } + } + + @action + void selectWalletGroup(WalletGroup walletGroup) { + selectedWalletGroup = walletGroup; + selectedSingleWallet = null; + } + + @action + void selectSingleWallet(WalletInfo singleWallet) { + selectedSingleWallet = singleWallet; + selectedWalletGroup = null; + } + + @action + void updateWalletInfoSourceList() { + List wallets = []; + + multiWalletGroups.clear(); + singleWalletsList.clear(); + + _walletManager.updateWalletGroups(); + + final walletGroups = _walletManager.walletGroups; + + // Iterate through the wallet groups to filter and categorize wallets + for (var group in walletGroups) { + // Handle group wallet filtering + bool shouldExcludeGroup = group.wallets.any((wallet) { + // Check for non-BIP39 wallet types + bool isNonBIP39Wallet = !isBIP39Wallet(wallet.type); + + // Check for nano derivation type + bool isNanoDerivationType = wallet.type == WalletType.nano && + wallet.derivationInfo?.derivationType == DerivationType.nano; + + // Check for electrum derivation type + bool isElectrumDerivationType = + (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) && + wallet.derivationInfo?.derivationType == DerivationType.electrum; + + // Check that selected wallet type is not present already in group + bool isSameTypeAsSelectedWallet = wallet.type == type; + + // Exclude if any of these conditions are true + return isNonBIP39Wallet || + isNanoDerivationType || + isElectrumDerivationType || + isSameTypeAsSelectedWallet; + }); + + if (shouldExcludeGroup) continue; + + // If the group passes the filters, add it to the wallets list + wallets.add(group); + } + + for (var group in wallets) { + if (group.wallets.length == 1) { + singleWalletsList.add(group.wallets.first); + } else { + multiWalletGroups.add(group); + } + } + } + + WalletListItem convertWalletInfoToWalletListItem(WalletInfo info) { + return WalletListItem( + name: info.name, + type: info.type, + key: info.key, + isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, + isEnabled: availableWalletTypes.contains(info.type), + isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, + ); + } +} diff --git a/lib/view_model/wallet_list/wallet_edit_view_model.dart b/lib/view_model/wallet_list/wallet_edit_view_model.dart index e5bfcd4e3..343f160db 100644 --- a/lib/view_model/wallet_list/wallet_edit_view_model.dart +++ b/lib/view_model/wallet_list/wallet_edit_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/di.dart'; @@ -18,8 +19,11 @@ class WalletEditRenamePending extends WalletEditViewModelState {} class WalletEditDeletePending extends WalletEditViewModelState {} abstract class WalletEditViewModelBase with Store { - WalletEditViewModelBase(this._walletListViewModel, this._walletLoadingService) - : state = WalletEditViewModelInitialState(), + WalletEditViewModelBase( + this._walletListViewModel, + this._walletLoadingService, + this._walletManager, + ) : state = WalletEditViewModelInitialState(), newName = ''; @observable @@ -30,13 +34,30 @@ abstract class WalletEditViewModelBase with Store { final WalletListViewModel _walletListViewModel; final WalletLoadingService _walletLoadingService; + final WalletManager _walletManager; @action - Future changeName(WalletListItem walletItem, {String? password}) async { + Future changeName( + WalletListItem walletItem, { + String? password, + String? groupParentAddress, + bool isWalletGroup = false, + }) async { state = WalletEditRenamePending(); - await _walletLoadingService.renameWallet( - walletItem.type, walletItem.name, newName, - password: password); + + if (isWalletGroup) { + _walletManager.updateWalletGroups(); + + _walletManager.setGroupName(groupParentAddress!, newName); + } else { + await _walletLoadingService.renameWallet( + walletItem.type, + walletItem.name, + newName, + password: password, + ); + } + _walletListViewModel.updateList(); } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 2ed6358f4..539f5c640 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -1,5 +1,7 @@ import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/wallet_group.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -17,7 +19,10 @@ abstract class WalletListViewModelBase with Store { this._walletInfoSource, this._appStore, this._walletLoadingService, - ) : wallets = ObservableList() { + this._walletManager, + ) : wallets = ObservableList(), + multiWalletGroups = ObservableList(), + singleWalletsList = ObservableList() { setOrderType(_appStore.settingsStore.walletListOrder); reaction((_) => _appStore.wallet, (_) => updateList()); updateList(); @@ -26,6 +31,15 @@ abstract class WalletListViewModelBase with Store { @observable ObservableList wallets; + // @observable + // ObservableList walletGroups; + + @observable + ObservableList multiWalletGroups; + + @observable + ObservableList singleWalletsList; + @computed bool get shouldRequireTOTP2FAForAccessingWallet => _appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; @@ -35,6 +49,7 @@ abstract class WalletListViewModelBase with Store { _appStore.settingsStore.shouldRequireTOTP2FAForCreatingNewWallets; final AppStore _appStore; + final WalletManager _walletManager; final Box _walletInfoSource; final WalletLoadingService _walletLoadingService; @@ -53,18 +68,23 @@ abstract class WalletListViewModelBase with Store { @action void updateList() { wallets.clear(); + multiWalletGroups.clear(); + singleWalletsList.clear(); + wallets.addAll( - _walletInfoSource.values.map( - (info) => WalletListItem( - name: info.name, - type: info.type, - key: info.key, - isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, - isEnabled: availableWalletTypes.contains(info.type), - isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, - ), - ), + _walletInfoSource.values.map((info) => convertWalletInfoToWalletListItem(info)), ); + + //========== Split into shared seed groups and single wallets list + _walletManager.updateWalletGroups(); + + for (var group in _walletManager.walletGroups) { + if (group.wallets.length == 1) { + singleWalletsList.add(convertWalletInfoToWalletListItem(group.wallets.first)); + } else { + multiWalletGroups.add(group); + } + } } Future reorderAccordingToWalletList() async { @@ -158,4 +178,15 @@ abstract class WalletListViewModelBase with Store { break; } } + + WalletListItem convertWalletInfoToWalletListItem(WalletInfo info) { + return WalletListItem( + name: info.name, + type: info.type, + key: info.key, + isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, + isEnabled: availableWalletTypes.contains(info.type), + isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, + ); + } } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 8c0f4f95f..2919afcac 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,8 +1,9 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/entities/seed_type.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; @@ -28,16 +29,17 @@ class WalletNewVM = WalletNewVMBase with _$WalletNewVM; abstract class WalletNewVMBase extends WalletCreationVM with Store { WalletNewVMBase( - AppStore appStore, - WalletCreationService walletCreationService, - Box walletInfoSource, - this.advancedPrivacySettingsViewModel, - SeedSettingsViewModel seedSettingsViewModel, - {required WalletType type}) - : selectedMnemonicLanguage = '', + AppStore appStore, + WalletCreationService walletCreationService, + Box walletInfoSource, + this.advancedPrivacySettingsViewModel, + SeedSettingsViewModel seedSettingsViewModel, { + required this.newWalletArguments, + }) : selectedMnemonicLanguage = '', super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, - type: type, isRecovery: false); + type: newWalletArguments!.type, isRecovery: false); + final NewWalletArguments? newWalletArguments; final AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel; @observable @@ -62,6 +64,10 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return seedSettingsViewModel.bitcoinSeedType == BitcoinSeedType.bip39 ? advancedPrivacySettingsViewModel.seedPhraseLength.value : 24; + case WalletType.nano: + return seedSettingsViewModel.nanoSeedType == NanoSeedType.bip39 + ? advancedPrivacySettingsViewModel.seedPhraseLength.value + : 24; default: return 24; } @@ -83,31 +89,68 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, isPolyseed: options.last as bool); case WalletType.bitcoin: - return bitcoin!.createBitcoinNewWalletCredentials( - name: name, password: walletPassword, passphrase: passphrase); case WalletType.litecoin: return bitcoin!.createBitcoinNewWalletCredentials( - name: name, password: walletPassword, passphrase: passphrase); + name: name, + password: walletPassword, + passphrase: passphrase, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + ); case WalletType.haven: return haven!.createHavenNewWalletCredentials( name: name, language: options!.first as String, password: walletPassword); case WalletType.ethereum: - return ethereum!.createEthereumNewWalletCredentials(name: name, password: walletPassword); + return ethereum!.createEthereumNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashNewWalletCredentials( - name: name, password: walletPassword, passphrase: passphrase); + name: name, + password: walletPassword, + passphrase: passphrase, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + ); case WalletType.nano: case WalletType.banano: - return nano!.createNanoNewWalletCredentials(name: name); + return nano!.createNanoNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + ); case WalletType.polygon: - return polygon!.createPolygonNewWalletCredentials(name: name, password: walletPassword); + return polygon!.createPolygonNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + ); case WalletType.solana: - return solana!.createSolanaNewWalletCredentials(name: name, password: walletPassword); + return solana!.createSolanaNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + ); case WalletType.tron: - return tron!.createTronNewWalletCredentials(name: name); + return tron!.createTronNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + ); case WalletType.wownero: return wownero!.createWowneroNewWalletCredentials( - name: name, language: options!.first as String, isPolyseed: options.last as bool); + name: name, + password: walletPassword, + language: options!.first as String, + isPolyseed: options.last as bool, + ); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 4784bb5e3..20ad96eef 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -129,7 +129,7 @@ "choose_from_available_options": "اختر من بين الخيارات المتاحة:", "choose_one": "اختر واحدة", "choose_relay": "ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ", - "choose_wallet_currency": "الرجاء اختيار عملة المحفظة:", + "choose_wallet_group": "اختر مجموعة المحفظة", "clear": "مسح", "clearnet_link": "رابط Clearnet", "close": "يغلق", @@ -176,6 +176,7 @@ "create_invoice": "إنشاء فاتورة", "create_new": "إنشاء محفظة جديدة", "create_new_account": "انشاء حساب جديد", + "create_new_seed": "إنشاء بذرة جديدة", "creating_new_wallet": "يتم إنشاء محفظة جديدة", "creating_new_wallet_error": "خطأ: ${description}", "creation_date": "تاريخ الإنشاء", @@ -600,6 +601,8 @@ "seed_share": "شارك السييد", "seed_title": "سييد", "seedtype": "البذور", + "seedtype_alert_content": "مشاركة البذور مع محافظ أخرى ممكن فقط مع BIP39 Seedtype.", + "seedtype_alert_title": "تنبيه البذور", "seedtype_legacy": "إرث (25 كلمة)", "seedtype_polyseed": "بوليسيد (16 كلمة)", "select_backup_file": "حدد ملف النسخ الاحتياطي", @@ -666,6 +669,7 @@ "setup_your_debit_card": "قم بإعداد بطاقة ائتمان الخاصة بك", "share": "يشارك", "share_address": "شارك العنوان", + "shared_seed_wallet_groups": "مجموعات محفظة البذور المشتركة", "show_details": "اظهر التفاصيل", "show_keys": "اظهار السييد / المفاتيح", "show_market_place": "إظهار السوق", @@ -690,6 +694,7 @@ "silent_payments_scanned_tip": "ممسوح ليفحص! (${tip})", "silent_payments_scanning": "المدفوعات الصامتة المسح الضوئي", "silent_payments_settings": "إعدادات المدفوعات الصامتة", + "single_seed_wallets_group": "محافظ بذرة واحدة", "slidable": "قابل للانزلاق", "sort_by": "ترتيب حسب", "spend_key_private": "مفتاح الإنفاق (خاص)", @@ -849,8 +854,16 @@ "view_transaction_on": "عرض العملية على", "voting_weight": "وزن التصويت", "waitFewSecondForTxUpdate": "ﺕﻼﻣﺎﻌﻤﻟﺍ ﻞﺠﺳ ﻲﻓ ﺔﻠﻣﺎﻌﻤﻟﺍ ﺲﻜﻌﻨﺗ ﻰﺘﺣ ﻥﺍﻮﺛ ﻊﻀﺒﻟ ﺭﺎﻈﺘﻧﻻﺍ ﻰﺟﺮﻳ", + "wallet_group": "مجموعة محفظة", + "wallet_group_description_four": "لإنشاء محفظة مع بذرة جديدة تماما.", + "wallet_group_description_one": "في محفظة الكيك ، يمكنك إنشاء ملف", + "wallet_group_description_three": "لرؤية المحافظ المتاحة و/أو شاشة مجموعات المحفظة. أو اختر", + "wallet_group_description_two": "عن طريق اختيار محفظة موجودة لتبادل البذور مع. يمكن أن تحتوي كل مجموعة محفظة على محفظة واحدة من كل نوع من العملة. \n\n يمكنك تحديدها", + "wallet_group_empty_state_text_one": "يبدو أنه ليس لديك أي مجموعات محفظة متوافقة !\n\n انقر", + "wallet_group_empty_state_text_two": "أدناه لجعل واحدة جديدة.", "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}", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index d76e1c763..d07ed0509 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -130,6 +130,7 @@ "choose_one": "Изберете едно", "choose_relay": "Моля, изберете реле, което да използвате", "choose_wallet_currency": "Изберете валута за портфейла:", + "choose_wallet_group": "Изберете Group Wallet", "clear": "Изчисти", "clearnet_link": "Clearnet връзка", "close": "затвори", @@ -176,6 +177,7 @@ "create_invoice": "Създайте фактура", "create_new": "Създаване на нов портфейл", "create_new_account": "Създаване на нов профил", + "create_new_seed": "Създайте нови семена", "creating_new_wallet": "Създаване на нов портфейл", "creating_new_wallet_error": "Грешка: ${description}", "creation_date": "Дата на създаване", @@ -600,6 +602,8 @@ "seed_share": "Споделяне на seed", "seed_title": "Seed", "seedtype": "Семенна тип", + "seedtype_alert_content": "Споделянето на семена с други портфейли е възможно само с BIP39 Seedtype.", + "seedtype_alert_title": "Сигнал за семена", "seedtype_legacy": "Наследство (25 думи)", "seedtype_polyseed": "Поли семе (16 думи)", "select_backup_file": "Избор на резервно копие", @@ -666,6 +670,7 @@ "setup_your_debit_card": "Настройте своята дебитна карта", "share": "Дял", "share_address": "Сподели адрес", + "shared_seed_wallet_groups": "Споделени групи за портфейли за семена", "show_details": "Показване на подробностите", "show_keys": "Покажи seed/keys", "show_market_place": "Покажи пазар", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "Сканиран за съвет! (${tip})", "silent_payments_scanning": "Безшумни плащания за сканиране", "silent_payments_settings": "Настройки за безшумни плащания", + "single_seed_wallets_group": "Портфейли с единични семена", "slidable": "Плъзгащ се", "sort_by": "Сортирай по", "spend_key_private": "Spend key (таен)", @@ -849,8 +855,16 @@ "view_transaction_on": "Вижте транзакция на ", "voting_weight": "Тегло на гласуване", "waitFewSecondForTxUpdate": "Моля, изчакайте няколко секунди, докато транзакцията се отрази в историята на транзакциите", + "wallet_group": "Група на портфейла", + "wallet_group_description_four": "За да създадете портфейл с изцяло ново семе.", + "wallet_group_description_one": "В портфейла за торта можете да създадете a", + "wallet_group_description_three": "За да видите наличния екран за портфейли и/или групи за портфейли. Или изберете", + "wallet_group_description_two": "Чрез избора на съществуващ портфейл, с който да споделите семе. Всяка група за портфейл може да съдържа по един портфейл от всеки тип валута. \n\n Можете да изберете", + "wallet_group_empty_state_text_one": "Изглежда, че нямате съвместими групи портфейли !\n\n tap", + "wallet_group_empty_state_text_two": "по -долу, за да се направи нов.", "wallet_keys": "Seed/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}", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index d4c9e1151..40ca8fb2c 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -130,6 +130,7 @@ "choose_one": "Zvolte si", "choose_relay": "Vyberte relé, které chcete použít", "choose_wallet_currency": "Prosím zvolte si měnu pro peněženku:", + "choose_wallet_group": "Vyberte skupinu peněženky", "clear": "Smazat", "clearnet_link": "Odkaz na Clearnet", "close": "zavřít", @@ -176,6 +177,7 @@ "create_invoice": "Vytvořit fakturu", "create_new": "Vytvořit novou peněženku", "create_new_account": "Vytvořit nový účet", + "create_new_seed": "Vytvořte nové semeno", "creating_new_wallet": "Vytvářím novou peněženku", "creating_new_wallet_error": "Chyba: ${description}", "creation_date": "Datum vzniku", @@ -600,6 +602,8 @@ "seed_share": "Sdílet seed", "seed_title": "Seed", "seedtype": "SeedType", + "seedtype_alert_content": "Sdílení semen s jinými peněženkami je možné pouze u BIP39 SeedType.", + "seedtype_alert_title": "Upozornění seedtype", "seedtype_legacy": "Legacy (25 slov)", "seedtype_polyseed": "Polyseed (16 slov)", "select_backup_file": "Vybrat soubor se zálohou", @@ -666,6 +670,7 @@ "setup_your_debit_card": "Nastavit debetní kartu", "share": "Podíl", "share_address": "Sdílet adresu", + "shared_seed_wallet_groups": "Skupiny sdílených semen", "show_details": "Zobrazit detaily", "show_keys": "Zobrazit seed/klíče", "show_market_place": "Zobrazit trh", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "Naskenované na tip! (${tip})", "silent_payments_scanning": "Skenování tichých plateb", "silent_payments_settings": "Nastavení tichých plateb", + "single_seed_wallets_group": "Jednorázové peněženky", "slidable": "Posuvné", "sort_by": "Seřazeno podle", "spend_key_private": "Klíč pro platby (soukromý)", @@ -849,8 +855,16 @@ "view_transaction_on": "Zobrazit transakci na ", "voting_weight": "Hlasová váha", "waitFewSecondForTxUpdate": "Počkejte několik sekund, než se transakce projeví v historii transakcí", + "wallet_group": "Skupina peněženky", + "wallet_group_description_four": "Vytvoření peněženky s zcela novým semenem.", + "wallet_group_description_one": "V peněžence dortu můžete vytvořit a", + "wallet_group_description_three": "Chcete -li zobrazit dostupnou obrazovku Skupina skupin peněženek a/nebo skupin peněženek. Nebo zvolit", + "wallet_group_description_two": "Výběrem existující peněženky pro sdílení semeno. Každá skupina peněženek může obsahovat jednu peněženku každého typu měny. \n\n Můžete si vybrat", + "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_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", "wallet_list_edit_wallet": "Upravit peněženku", "wallet_list_failed_to_load": "Chyba při načítání ${wallet_name} peněženky. ${error}", "wallet_list_failed_to_remove": "Chyba při odstraňování ${wallet_name} peněženky. ${error}", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 8b316fe97..3c7e9fbc9 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -130,6 +130,7 @@ "choose_one": "Wähle ein", "choose_relay": "Bitte wählen Sie ein zu verwendendes Relais aus", "choose_wallet_currency": "Bitte wählen Sie die Währung der Wallet:", + "choose_wallet_group": "Wählen Sie Brieftaschengruppe", "clear": "Zurücksetzen", "clearnet_link": "Clearnet-Link", "close": "Schließen", @@ -176,6 +177,7 @@ "create_invoice": "Rechnung erstellen", "create_new": "Neue Wallet erstellen", "create_new_account": "Neues Konto erstellen", + "create_new_seed": "Neue Samen erstellen", "creating_new_wallet": "Neue Wallet erstellen", "creating_new_wallet_error": "Fehler: ${description}", "creation_date": "Erstellungsdatum", @@ -601,6 +603,8 @@ "seed_share": "Seed teilen", "seed_title": "Seed", "seedtype": "Seedtyp", + "seedtype_alert_content": "Das Teilen von Samen mit anderen Brieftaschen ist nur mit bip39 Seedype möglich.", + "seedtype_alert_title": "Seedype -Alarm", "seedtype_legacy": "Veraltet (25 Wörter)", "seedtype_polyseed": "Polyseed (16 Wörter)", "select_backup_file": "Sicherungsdatei auswählen", @@ -667,6 +671,7 @@ "setup_your_debit_card": "Richten Sie Ihre Debitkarte ein", "share": "Teilen", "share_address": "Adresse teilen ", + "shared_seed_wallet_groups": "Gemeinsame Samenbrieftaschengruppen", "show_details": "Details anzeigen", "show_keys": "Seed/Schlüssel anzeigen", "show_market_place": "Marktplatz anzeigen", @@ -691,6 +696,7 @@ "silent_payments_scanned_tip": "Gescannt zum Trinkgeld! (${tip})", "silent_payments_scanning": "Stille Zahlungen scannen", "silent_payments_settings": "Einstellungen für stille Zahlungen", + "single_seed_wallets_group": "Einzelne Samenbriefen", "slidable": "Verschiebbar", "sort_by": "Sortiere nach", "spend_key_private": "Spend Key (geheim)", @@ -852,8 +858,16 @@ "voting_weight": "Stimmgewicht", "waitFewSecondForTxUpdate": "Bitte warten Sie einige Sekunden, bis die Transaktion im Transaktionsverlauf angezeigt wird", "waiting_payment_confirmation": "Warte auf Zahlungsbestätigung", + "wallet_group": "Brieftaschengruppe", + "wallet_group_description_four": "eine Brieftasche mit einem völlig neuen Samen schaffen.", + "wallet_group_description_one": "In Kuchenbrieftasche können Sie eine erstellen", + "wallet_group_description_three": "Sehen Sie den Bildschirm zur verfügbaren Brieftaschen und/oder Brieftaschengruppen. Oder wählen", + "wallet_group_description_two": "Durch die Auswahl einer vorhandenen Brieftasche, mit der ein Samen geteilt werden kann. Jede Brieftaschengruppe kann eine einzelne Brieftasche jedes Währungstyps enthalten. \n\n Sie können auswählen", + "wallet_group_empty_state_text_one": "Sieht so aus, als hätten Sie keine kompatiblen Brieftaschengruppen !\n\n TAP", + "wallet_group_empty_state_text_two": "unten, um einen neuen zu machen.", "wallet_keys": "Wallet-Seed/-Schlüssel", "wallet_list_create_new_wallet": "Neue Wallet erstellen", + "wallet_list_edit_group_name": "Gruppenname bearbeiten", "wallet_list_edit_wallet": "Wallet bearbeiten", "wallet_list_failed_to_load": "Laden der Wallet ${wallet_name} fehlgeschlagen. ${error}", "wallet_list_failed_to_remove": "Fehler beim Entfernen der Wallet ${wallet_name}. ${error}", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 6858d9359..7b98ed3d6 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -130,6 +130,7 @@ "choose_one": "Choose one", "choose_relay": "Please choose a relay to use", "choose_wallet_currency": "Please choose wallet currency:", + "choose_wallet_group": "Choose Wallet Group", "clear": "Clear", "clearnet_link": "Clearnet link", "close": "Close", @@ -176,6 +177,7 @@ "create_invoice": "Create invoice", "create_new": "Create New Wallet", "create_new_account": "Create new account", + "create_new_seed": "Create New Seed", "creating_new_wallet": "Creating new wallet", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Creation Date", @@ -600,6 +602,8 @@ "seed_share": "Share seed", "seed_title": "Seed", "seedtype": "Seedtype", + "seedtype_alert_content": "Sharing seeds with other wallets is only possible with BIP39 SeedType.", + "seedtype_alert_title": "SeedType Alert", "seedtype_legacy": "Legacy (25 words)", "seedtype_polyseed": "Polyseed (16 words)", "seedtype_wownero": "Wownero (14 words)", @@ -667,6 +671,7 @@ "setup_your_debit_card": "Set up your debit card", "share": "Share", "share_address": "Share address", + "shared_seed_wallet_groups": "Shared Seed Wallet Groups", "show_details": "Show Details", "show_keys": "Show seed/keys", "show_market_place": "Show Marketplace", @@ -691,6 +696,7 @@ "silent_payments_scanned_tip": "SCANNED TO TIP! (${tip})", "silent_payments_scanning": "Silent Payments Scanning", "silent_payments_settings": "Silent Payments settings", + "single_seed_wallets_group": "Single Seed Wallets", "slidable": "Slidable", "sort_by": "Sort by", "spend_key_private": "Spend key (private)", @@ -850,8 +856,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Voting Weight", "waitFewSecondForTxUpdate": "Kindly wait for a few seconds for transaction to reflect in transactions history", + "wallet_group": "Wallet Group", + "wallet_group_description_four": "to create a wallet with an entirely new seed.", + "wallet_group_description_one": "In Cake Wallet, you can create a", + "wallet_group_description_three": "to see the available wallets and/or wallet groups screen. Or choose", + "wallet_group_description_two": "by selecting an existing wallet to share a seed with. Each wallet group can contain a single wallet of each currency type.\n\nYou can select", + "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_keys": "Wallet seed/keys", "wallet_list_create_new_wallet": "Create New Wallet", + "wallet_list_edit_group_name": "Edit Group Name", "wallet_list_edit_wallet": "Edit wallet", "wallet_list_failed_to_load": "Failed to load ${wallet_name} wallet. ${error}", "wallet_list_failed_to_remove": "Failed to remove ${wallet_name} wallet. ${error}", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index ab92499f7..4043e5ac3 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -130,6 +130,7 @@ "choose_one": "Elige uno", "choose_relay": "Por favor elija un relé para usar", "choose_wallet_currency": "Por favor, elija la moneda de la billetera:", + "choose_wallet_group": "Elija el grupo de billetera", "clear": "Claro", "clearnet_link": "enlace Clearnet", "close": "Cerca", @@ -176,6 +177,7 @@ "create_invoice": "Crear factura", "create_new": "Crear nueva billetera", "create_new_account": "Crear una nueva cuenta", + "create_new_seed": "Crear nueva semilla", "creating_new_wallet": "Creando nueva billetera", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Fecha de creación", @@ -601,6 +603,8 @@ "seed_share": "Compartir semillas", "seed_title": "Semilla", "seedtype": "Type de semillas", + "seedtype_alert_content": "Compartir semillas con otras billeteras solo es posible con Bip39 Seed Type.", + "seedtype_alert_title": "Alerta de type de semillas", "seedtype_legacy": "Legado (25 palabras)", "seedtype_polyseed": "Polieta (16 palabras)", "select_backup_file": "Seleccionar archivo de respaldo", @@ -667,6 +671,7 @@ "setup_your_debit_card": "Configura tu tarjeta de débito", "share": "Compartir", "share_address": "Compartir dirección", + "shared_seed_wallet_groups": "Grupos de billetera de semillas compartidas", "show_details": "Mostrar detalles", "show_keys": "Mostrar semilla/claves", "show_market_place": "Mostrar mercado", @@ -691,6 +696,7 @@ "silent_payments_scanned_tip": "Escaneado hasta la punta! (${tip})", "silent_payments_scanning": "Escaneo de pagos silenciosos", "silent_payments_settings": "Configuración de pagos silenciosos", + "single_seed_wallets_group": "Billeteras de semillas individuales", "slidable": "deslizable", "sort_by": "Ordenar por", "spend_key_private": "Spend clave (privado)", @@ -850,8 +856,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Peso de votación", "waitFewSecondForTxUpdate": "Espere unos segundos para que la transacción se refleje en el historial de transacciones.", + "wallet_group": "Grupo de billetera", + "wallet_group_description_four": "para crear una billetera con una semilla completamente nueva.", + "wallet_group_description_one": "En la billetera de pastel, puedes crear un", + "wallet_group_description_three": "Para ver las billeteras disponibles y/o la pantalla de grupos de billeteras. O elegir", + "wallet_group_description_two": "seleccionando una billetera existente para compartir una semilla con. Cada grupo de billetera puede contener una sola billetera de cada tipo de moneda. \n\n puede seleccionar", + "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_keys": "Billetera semilla/claves", "wallet_list_create_new_wallet": "Crear nueva billetera", + "wallet_list_edit_group_name": "Editar nombre de grupo", "wallet_list_edit_wallet": "Editar billetera", "wallet_list_failed_to_load": "No se pudo cargar ${wallet_name} la billetera. ${error}", "wallet_list_failed_to_remove": "Error al elimina ${wallet_name} billetera. ${error}", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index d062cdd86..7bbc1076b 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -130,6 +130,7 @@ "choose_one": "Choisissez-en un", "choose_relay": "Veuillez choisir un relais à utiliser", "choose_wallet_currency": "Merci de choisir la devise du portefeuille (wallet) :", + "choose_wallet_group": "Choisissez un groupe de portefeuille", "clear": "Effacer", "clearnet_link": "Lien Clearnet", "close": "Fermer", @@ -176,6 +177,7 @@ "create_invoice": "Créer une facture", "create_new": "Créer un Nouveau Portefeuille (Wallet)", "create_new_account": "Créer un nouveau compte", + "create_new_seed": "Créer de nouvelles graines", "creating_new_wallet": "Création d'un nouveau portefeuille (wallet)", "creating_new_wallet_error": "Erreur : ${description}", "creation_date": "Date de création", @@ -600,6 +602,8 @@ "seed_share": "Partager la phrase secrète (seed)", "seed_title": "Phrase secrète (seed)", "seedtype": "Type de type graine", + "seedtype_alert_content": "Le partage de graines avec d'autres portefeuilles n'est possible qu'avec Bip39 SeedType.", + "seedtype_alert_title": "Alerte de type SeedType", "seedtype_legacy": "Héritage (25 mots)", "seedtype_polyseed": "Polyseed (16 mots)", "select_backup_file": "Sélectionnez le fichier de sauvegarde", @@ -666,6 +670,7 @@ "setup_your_debit_card": "Configurer votre carte de débit", "share": "Partager", "share_address": "Partager l'adresse", + "shared_seed_wallet_groups": "Groupes de portefeuilles partagés", "show_details": "Afficher les détails", "show_keys": "Visualiser la phrase secrète (seed) et les clefs", "show_market_place": "Afficher la place de marché", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "Scanné à la pointe! (${tip})", "silent_payments_scanning": "Payments silencieux SCANNING", "silent_payments_settings": "Paramètres de paiement silencieux", + "single_seed_wallets_group": "Portefeuilles de semences simples", "slidable": "Glissable", "sort_by": "Trier par", "spend_key_private": "Clef de dépense (spend key) (privée)", @@ -849,8 +855,16 @@ "view_transaction_on": "Voir la Transaction sur ", "voting_weight": "Poids de vote", "waitFewSecondForTxUpdate": "Veuillez attendre quelques secondes pour que la transaction soit reflétée dans l'historique des transactions.", + "wallet_group": "Groupe de portefeuille", + "wallet_group_description_four": "Pour créer un portefeuille avec une graine entièrement nouvelle.", + "wallet_group_description_one": "Dans Cake Wallet, vous pouvez créer un", + "wallet_group_description_three": "Pour voir les portefeuilles et / ou les groupes de portefeuilles disponibles. Ou choisir", + "wallet_group_description_two": "En sélectionnant un portefeuille existant pour partager une graine avec. Chaque groupe de portefeuille peut contenir un seul portefeuille de chaque type de devise. \n\n Vous pouvez sélectionner", + "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_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", "wallet_list_edit_wallet": "Modifier le portefeuille", "wallet_list_failed_to_load": "Échec de chargement du portefeuille (wallet) ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Échec de la suppression du portefeuille (wallet) ${wallet_name}. ${error}", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 017fd7e01..6f551a656 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -130,6 +130,7 @@ "choose_one": "Zaɓi ɗaya", "choose_relay": "Da fatan za a zaɓi gudun ba da sanda don amfani", "choose_wallet_currency": "Da fatan za a zaɓi kuɗin walat:", + "choose_wallet_group": "Zabi kungiyar walat", "clear": "Share", "clearnet_link": "Lambar makomar kwayoyi", "close": "Rufa", @@ -176,6 +177,7 @@ "create_invoice": "Sanya bayanin wadannan", "create_new": "Ƙirƙira Sabon Kwalinku", "create_new_account": "Ƙirƙiri sabon asusu", + "create_new_seed": "Irƙiri sabon iri", "creating_new_wallet": "Haliccin walat sabuwa", "creating_new_wallet_error": "Kuskure: ${description}", "creation_date": "Ranar halitta", @@ -602,6 +604,8 @@ "seed_share": "Raba iri", "seed_title": "iri", "seedtype": "Seedtype", + "seedtype_alert_content": "Raba tsaba tare da sauran wallets yana yiwuwa ne kawai tare da Bip39 seedtype.", + "seedtype_alert_title": "Seedtype farke", "seedtype_legacy": "Legacy (25 kalmomi)", "seedtype_polyseed": "Polyseed (16 kalmomi)", "select_backup_file": "Zaɓi fayil ɗin madadin", @@ -668,6 +672,7 @@ "setup_your_debit_card": "Saita katin zare kudi", "share": "Raba", "share_address": "Raba adireshin", + "shared_seed_wallet_groups": "Raba ƙungiya walat", "show_details": "Nuna Cikakkun bayanai", "show_keys": "Nuna iri/maɓallai", "show_market_place": "Nuna dan kasuwa", @@ -692,6 +697,7 @@ "silent_payments_scanned_tip": "Bincika don tip! (${tip})", "silent_payments_scanning": "Silent biya scanning", "silent_payments_settings": "Saitunan Silent", + "single_seed_wallets_group": "Guaro", "slidable": "Mai iya zamewa", "sort_by": "Kasa", "spend_key_private": "makullin biya (maɓallin kalmar sirri)", @@ -851,8 +857,16 @@ "view_transaction_on": "Dubo aikace-aikacen akan", "voting_weight": "Nauyi mai nauyi", "waitFewSecondForTxUpdate": "Da fatan za a jira ƴan daƙiƙa don ciniki don yin tunani a tarihin ma'amala", + "wallet_group": "Wallet kungiyar", + "wallet_group_description_four": "Don ƙirƙirar walat tare da sabon iri.", + "wallet_group_description_one": "A cikin walat walat, zaka iya ƙirƙirar", + "wallet_group_description_three": "Don ganin wallets da / ko allon walat din. Ko zabi", + "wallet_group_description_two": "ta hanyar zabar walat mai gudana don raba iri tare da. Kowane rukunin walat na iya ƙunsar watsarin kowane nau'in kuɗi. \n\n Zaka iya zaɓar", + "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_keys": "Iri/maɓalli na walat", "wallet_list_create_new_wallet": "Ƙirƙiri Sabon Wallet", + "wallet_list_edit_group_name": "Shirya sunan rukuni", "wallet_list_edit_wallet": "Gyara walat", "wallet_list_failed_to_load": "An kasa loda ${wallet_name} walat. ${error}", "wallet_list_failed_to_remove": "Ba a iya cirewa ${wallet_name} walat. ${error}", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 95eb228f4..8e3990552 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -130,6 +130,7 @@ "choose_one": "एक का चयन", "choose_relay": "कृपया उपयोग करने के लिए एक रिले चुनें", "choose_wallet_currency": "कृपया बटुआ मुद्रा चुनें:", + "choose_wallet_group": "वॉलेट ग्रुप चुनें", "clear": "स्पष्ट", "clearnet_link": "क्लियरनेट लिंक", "close": "बंद करना", @@ -176,6 +177,7 @@ "create_invoice": "इनवॉयस बनाएँ", "create_new": "नया बटुआ बनाएँ", "create_new_account": "नया खाता बनाएँ", + "create_new_seed": "नया बीज बनाएं", "creating_new_wallet": "नया बटुआ बनाना", "creating_new_wallet_error": "त्रुटि: ${description}", "creation_date": "निर्माण तिथि", @@ -602,6 +604,8 @@ "seed_share": "बीज साझा करें", "seed_title": "बीज", "seedtype": "बीज", + "seedtype_alert_content": "अन्य बटुए के साथ बीज साझा करना केवल BIP39 SEEDTYPE के साथ संभव है।", + "seedtype_alert_title": "बीजगणित अलर्ट", "seedtype_legacy": "विरासत (25 शब्द)", "seedtype_polyseed": "पॉलीसीड (16 शब्द)", "select_backup_file": "बैकअप फ़ाइल का चयन करें", @@ -668,6 +672,7 @@ "setup_your_debit_card": "अपना डेबिट कार्ड सेट करें", "share": "शेयर करना", "share_address": "पता साझा करें", + "shared_seed_wallet_groups": "साझा बीज बटुए समूह", "show_details": "विवरण दिखाएं", "show_keys": "बीज / कुंजियाँ दिखाएँ", "show_market_place": "बाज़ार दिखाएँ", @@ -692,6 +697,7 @@ "silent_payments_scanned_tip": "टिप करने के लिए स्कैन किया! (${tip})", "silent_payments_scanning": "मूक भुगतान स्कैनिंग", "silent_payments_settings": "मूक भुगतान सेटिंग्स", + "single_seed_wallets_group": "एकल बीज बटुए", "slidable": "फिसलने लायक", "sort_by": "इसके अनुसार क्रमबद्ध करें", "spend_key_private": "खर्च करना (निजी)", @@ -851,8 +857,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "वोटिंग वेट", "waitFewSecondForTxUpdate": "लेन-देन इतिहास में लेन-देन प्रतिबिंबित होने के लिए कृपया कुछ सेकंड प्रतीक्षा करें", + "wallet_group": "बटुए समूह", + "wallet_group_description_four": "एक पूरी तरह से नए बीज के साथ एक बटुआ बनाने के लिए।", + "wallet_group_description_one": "केक बटुए में, आप एक बना सकते हैं", + "wallet_group_description_three": "उपलब्ध वॉलेट और/या वॉलेट समूह स्क्रीन देखने के लिए। या चुनें", + "wallet_group_description_two": "एक बीज साझा करने के लिए एक मौजूदा बटुए का चयन करके। प्रत्येक वॉलेट समूह में प्रत्येक मुद्रा प्रकार का एक एकल वॉलेट हो सकता है। \n\n आप चयन कर सकते हैं", + "wallet_group_empty_state_text_one": "लगता है कि आपके पास कोई संगत बटुआ समूह नहीं है !\n\n टैप करें", + "wallet_group_empty_state_text_two": "नीचे एक नया बनाने के लिए।", "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}", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 28f1c05b1..648f996ec 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -130,6 +130,7 @@ "choose_one": "Izaberi jedan", "choose_relay": "Odaberite relej za korištenje", "choose_wallet_currency": "Molimo odaberite valutu novčanika:", + "choose_wallet_group": "Odaberite grupu novčanika", "clear": "Izbriši", "clearnet_link": "Clearnet veza", "close": "Zatvoriti", @@ -176,6 +177,7 @@ "create_invoice": "Izradite fakturu", "create_new": "Izradi novi novčanik", "create_new_account": "Izradi novi račun", + "create_new_seed": "Stvorite novo sjeme", "creating_new_wallet": "Stvaranje novog novčanika", "creating_new_wallet_error": "Greška: ${description}", "creation_date": "Datum stvaranja", @@ -600,6 +602,8 @@ "seed_share": "Podijeli pristupni izraz", "seed_title": "Prisupni izraz", "seedtype": "Sjemenska vrsta", + "seedtype_alert_content": "Dijeljenje sjemena s drugim novčanicima moguće je samo s BIP39 sjemenom.", + "seedtype_alert_title": "Upozorenje o sjemenu", "seedtype_legacy": "Nasljeđe (25 riječi)", "seedtype_polyseed": "Poliseed (16 riječi)", "select_backup_file": "Odaberite datoteku sigurnosne kopije", @@ -666,6 +670,7 @@ "setup_your_debit_card": "Postavite svoju debitnu karticu", "share": "Udio", "share_address": "Podijeli adresu", + "shared_seed_wallet_groups": "Zajedničke grupe za sjeme novčanika", "show_details": "Prikaži pojedinosti", "show_keys": "Prikaži pristupni izraz/ključ", "show_market_place": "Prikaži tržište", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "Skenirano na savjet! (${tip})", "silent_payments_scanning": "Skeniranje tihih plaćanja", "silent_payments_settings": "Postavke tihih plaćanja", + "single_seed_wallets_group": "Jednostruki novčanici", "slidable": "Klizna", "sort_by": "Poredaj po", "spend_key_private": "Spend key (privatni)", @@ -849,8 +855,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Težina glasanja", "waitFewSecondForTxUpdate": "Pričekajte nekoliko sekundi da se transakcija prikaže u povijesti transakcija", + "wallet_group": "Skupina novčanika", + "wallet_group_description_four": "Da biste stvorili novčanik s potpuno novim sjemenom.", + "wallet_group_description_one": "U novčaniku kolača možete stvoriti a", + "wallet_group_description_three": "Da biste vidjeli zaslon dostupnih novčanika i/ili grupa novčanika. Ili odaberite", + "wallet_group_description_two": "Odabirom postojećeg novčanika s kojim ćete dijeliti sjeme. Svaka grupa novčanika može sadržavati jedan novčanik svake vrste valute. \n\n", + "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_keys": "Pristupni izraz/ključ novčanika", "wallet_list_create_new_wallet": "Izradi novi novčanik", + "wallet_list_edit_group_name": "Uredi naziv grupe", "wallet_list_edit_wallet": "Uredi novčanik", "wallet_list_failed_to_load": "Neuspješno učitavanje novčanika ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Neuspješno uklanjanje novčanika ${wallet_name}. ${error}", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 56d45deb1..786b4cfee 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -130,6 +130,7 @@ "choose_one": "Ընտրեք մեկը", "choose_relay": "Խնդրում ենք ընտրեք փոխանցման կետ", "choose_wallet_currency": "Խնդրում ենք ընտրեք դրամապանակի արժույթը", + "choose_wallet_group": "Ընտրեք դրամապանակների խումբ", "clear": "Մաքրել", "clearnet_link": "Բաց ցանցի հղում", "close": "Փակել", @@ -176,6 +177,7 @@ "create_invoice": "Ստեղծել հաշիվ-ապրանքագիր", "create_new": "Ստեղծել նոր դրամապանակ", "create_new_account": "Ստեղծել նոր հաշիվ", + "create_new_seed": "Ստեղծեք նոր սերունդ", "creating_new_wallet": "Նոր դրամապանակ ստեղծվում է", "creating_new_wallet_error": "Սխալ: ${description}", "creation_date": "Ստեղծման ամսաթիվ", @@ -599,6 +601,8 @@ "seed_share": "Կիսվել սերմով", "seed_title": "Սերմ", "seedtype": "Սերմի տեսակ", + "seedtype_alert_content": "Այլ դրամապանակներով սերմերի փոխանակումը հնարավոր է միայն BIP39 SEEDTYPE- ով:", + "seedtype_alert_title": "SEEDTYPE ALERT", "seedtype_legacy": "Legacy (25 բառ)", "seedtype_polyseed": "Polyseed (16 բառ)", "seedtype_wownero": "Wownero (14 բառ)", @@ -666,6 +670,7 @@ "setup_your_debit_card": "Հավատարմագրել ձեր դեբետային քարտ", "share": "Կիսվել", "share_address": "Կիսվել հասցեով", + "shared_seed_wallet_groups": "Համօգտագործված սերմերի դրամապանակների խմբեր", "show_details": "Ցուցադրել մանրամասներ", "show_keys": "Ցուցադրել բանալիներ", "show_market_place": "Ցուցադրել շուկան", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "ՍԿԱՆԱՎՈՐՎԵՑ ԹԵՅԱՎՃԱՐ! (${tip})", "silent_payments_scanning": "Լուռ Վճարումներ Սկանավորում", "silent_payments_settings": "Լուռ Վճարումներ Կարգավորումներ", + "single_seed_wallets_group": "Մեկ սերմերի դրամապանակներ", "slidable": "Սահելի", "sort_by": "Դասավորել ըստ", "spend_key_private": "Վճարման բանալի (գախտնի)", @@ -849,8 +855,16 @@ "view_transaction_on": "Դիտել Գործարքը ", "voting_weight": "Քվեարկության Քաշ", "waitFewSecondForTxUpdate": "Խնդրում ենք սպասել մի քանի վայրկյան, որպեսզի գործարքը արտացոլվի գործարքների պատմության մեջ", + "wallet_group": "Դրամապանակների խումբ", + "wallet_group_description_four": "Ամբողջովին նոր սերմով դրամապանակ ստեղծելու համար:", + "wallet_group_description_one": "Տորթի դրամապանակում կարող եք ստեղծել ա", + "wallet_group_description_three": "Տեսնել առկա դրամապանակներն ու (կամ) դրամապանակների խմբերի էկրանը: Կամ ընտրել", + "wallet_group_description_two": "ընտրելով գոյություն ունեցող դրամապանակ `սերմը կիսելու համար: Դրամապանակների յուրաքանչյուր խումբ կարող է պարունակել յուրաքանչյուր արժույթի տիպի մեկ դրամապանակ: \n\n Կարող եք ընտրել", + "wallet_group_empty_state_text_one": "Կարծես թե դուք չունեք որեւէ համատեղելի դրամապանակների խմբեր !\n\n թակել", + "wallet_group_empty_state_text_two": "ներքեւում `նորը կազմելու համար:", "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}", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 77a2c68e2..530f78eef 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -130,6 +130,7 @@ "choose_one": "Pilih satu", "choose_relay": "Silakan pilih relai yang akan digunakan", "choose_wallet_currency": "Silahkan pilih mata uang dompet:", + "choose_wallet_group": "Pilih Grup Dompet", "clear": "Hapus", "clearnet_link": "Tautan clearnet", "close": "Menutup", @@ -176,6 +177,7 @@ "create_invoice": "Buat faktur", "create_new": "Buat Dompet Baru", "create_new_account": "Buat akun baru", + "create_new_seed": "Buat benih baru", "creating_new_wallet": "Membuat dompet baru", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Tanggal Pembuatan", @@ -603,6 +605,8 @@ "seed_share": "Bagikan bibit", "seed_title": "Bibit", "seedtype": "Seedtype", + "seedtype_alert_content": "Berbagi biji dengan dompet lain hanya dimungkinkan dengan BIP39 seedtype.", + "seedtype_alert_title": "Peringatan seedtype", "seedtype_legacy": "Legacy (25 kata)", "seedtype_polyseed": "Polyseed (16 kata)", "select_backup_file": "Pilih file cadangan", @@ -669,6 +673,7 @@ "setup_your_debit_card": "Pasang kartu debit Anda", "share": "Membagikan", "share_address": "Bagikan alamat", + "shared_seed_wallet_groups": "Kelompok dompet benih bersama", "show_details": "Tampilkan Rincian", "show_keys": "Tampilkan seed/kunci", "show_market_place": "Tampilkan Pasar", @@ -693,6 +698,7 @@ "silent_payments_scanned_tip": "Pindai untuk memberi tip! (${tip})", "silent_payments_scanning": "Pemindaian pembayaran diam", "silent_payments_settings": "Pengaturan pembayaran diam", + "single_seed_wallets_group": "Dompet biji tunggal", "slidable": "Dapat digeser", "sort_by": "Sortir dengan", "spend_key_private": "Kunci pengeluaran (privat)", @@ -852,8 +858,16 @@ "view_transaction_on": "Lihat Transaksi di ", "voting_weight": "Berat voting", "waitFewSecondForTxUpdate": "Mohon tunggu beberapa detik hingga transaksi terlihat di riwayat transaksi", + "wallet_group": "Kelompok dompet", + "wallet_group_description_four": "Untuk membuat dompet dengan benih yang sama sekali baru.", + "wallet_group_description_one": "Di dompet kue, Anda dapat membuat file", + "wallet_group_description_three": "Untuk melihat layar dompet dan/atau grup dompet yang tersedia. Atau pilih", + "wallet_group_description_two": "dengan memilih dompet yang ada untuk berbagi benih dengan. Setiap grup dompet dapat berisi satu dompet dari setiap jenis mata uang. \n\n Anda dapat memilih", + "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_keys": "Seed/kunci dompet", "wallet_list_create_new_wallet": "Buat Dompet Baru", + "wallet_list_edit_group_name": "Edit Nama Grup", "wallet_list_edit_wallet": "Edit dompet", "wallet_list_failed_to_load": "Gagal memuat ${wallet_name} dompet. ${error}", "wallet_list_failed_to_remove": "Gagal menghapus ${wallet_name} dompet. ${error}", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 161f46ad9..6257d4800 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -130,6 +130,7 @@ "choose_one": "Scegline uno", "choose_relay": "Scegli un relè da utilizzare", "choose_wallet_currency": "Gentilmente scegli la moneta del portafoglio:", + "choose_wallet_group": "Scegli il gruppo del portafoglio", "clear": "Pulisci", "clearnet_link": "Collegamento Clearnet", "close": "Chiudere", @@ -177,6 +178,7 @@ "create_invoice": "Crea fattura", "create_new": "Genera nuovo Portafoglio", "create_new_account": "Crea nuovo account", + "create_new_seed": "Crea nuovo seme", "creating_new_wallet": "Creazione nuovo portafoglio", "creating_new_wallet_error": "Errore: ${description}", "creation_date": "Data di creazione", @@ -602,6 +604,8 @@ "seed_share": "Condividi seme", "seed_title": "Seme", "seedtype": "Seedtype", + "seedtype_alert_content": "La condivisione di semi con altri portafogli è possibile solo con Bip39 SeedType.", + "seedtype_alert_title": "Avviso seedType", "seedtype_legacy": "Legacy (25 parole)", "seedtype_polyseed": "Polyseed (16 parole)", "select_backup_file": "Seleziona file di backup", @@ -668,6 +672,7 @@ "setup_your_debit_card": "Configura la tua carta di debito", "share": "Condividere", "share_address": "Condividi indirizzo", + "shared_seed_wallet_groups": "Gruppi di portafoglio di semi condivisi", "show_details": "Mostra dettagli", "show_keys": "Mostra seme/chiavi", "show_market_place": "Mostra mercato", @@ -692,6 +697,7 @@ "silent_payments_scanned_tip": "Scansionato per dare la mancia! (${tip})", "silent_payments_scanning": "Scansione di pagamenti silenziosi", "silent_payments_settings": "Impostazioni di pagamenti silenziosi", + "single_seed_wallets_group": "Portafogli singoli", "slidable": "Scorrevole", "sort_by": "Ordina per", "spend_key_private": "Chiave di spesa (privata)", @@ -852,8 +858,16 @@ "voting_weight": "Peso di voto", "waitFewSecondForTxUpdate": "Attendi qualche secondo affinché la transazione venga riflessa nella cronologia delle transazioni", "waiting_payment_confirmation": "In attesa di conferma del pagamento", + "wallet_group": "Gruppo di portafoglio", + "wallet_group_description_four": "Per creare un portafoglio con un seme completamente nuovo.", + "wallet_group_description_one": "Nel portafoglio di torte, puoi creare un", + "wallet_group_description_three": "Per vedere la schermata di portafogli e/o gruppi di portafogli disponibili. O scegli", + "wallet_group_description_two": "Selezionando un portafoglio esistente con cui condividere un seme. Ogni gruppo di portafoglio può contenere un singolo portafoglio di ciascun tipo di valuta. \n\n È possibile selezionare", + "wallet_group_empty_state_text_one": "Sembra che tu non abbia alcun gruppo di portafoglio compatibile !\n\n TAP", + "wallet_group_empty_state_text_two": "Di seguito per crearne uno nuovo.", "wallet_keys": "Seme Portafoglio /chiavi", "wallet_list_create_new_wallet": "Crea Nuovo Portafoglio", + "wallet_list_edit_group_name": "Modifica nome del gruppo", "wallet_list_edit_wallet": "Modifica portafoglio", "wallet_list_failed_to_load": "Caricamento portafoglio ${wallet_name} fallito. ${error}", "wallet_list_failed_to_remove": "Rimozione portafoglio ${wallet_name} fallita. ${error}", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 8c465f2f7..e30409cdf 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -130,6 +130,7 @@ "choose_one": "1 つ選択してください", "choose_relay": "使用するリレーを選択してください", "choose_wallet_currency": "ウォレット通貨を選択してください:", + "choose_wallet_group": "ウォレットグループを選択してください", "clear": "クリア", "clearnet_link": "クリアネット リンク", "close": "近い", @@ -176,6 +177,7 @@ "create_invoice": "請求書の作成", "create_new": "新しいウォレットを作成", "create_new_account": "新しいアカウントを作成する", + "create_new_seed": "新しい種を作成します", "creating_new_wallet": "新しいウォレットの作成", "creating_new_wallet_error": "エラー: ${description}", "creation_date": "作成日", @@ -601,6 +603,8 @@ "seed_share": "シードを共有する", "seed_title": "シード", "seedtype": "SeedType", + "seedtype_alert_content": "他の財布と種子を共有することは、BIP39 SeedTypeでのみ可能です。", + "seedtype_alert_title": "SeedTypeアラート", "seedtype_legacy": "レガシー(25語)", "seedtype_polyseed": "ポリシード(16語)", "select_backup_file": "バックアップファイルを選択", @@ -667,6 +671,7 @@ "setup_your_debit_card": "デビットカードを設定してください", "share": "共有", "share_address": "住所を共有する", + "shared_seed_wallet_groups": "共有シードウォレットグループ", "show_details": "詳細を表示", "show_keys": "シード/キーを表示する", "show_market_place": "マーケットプレイスを表示", @@ -691,6 +696,7 @@ "silent_payments_scanned_tip": "チップをスキャンしました! (${tip})", "silent_payments_scanning": "サイレントペイメントスキャン", "silent_payments_settings": "サイレントペイメント設定", + "single_seed_wallets_group": "シングルシードウォレット", "slidable": "スライド可能", "sort_by": "並び替え", "spend_key_private": "キーを使う (プライベート)", @@ -850,8 +856,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "投票重み", "waitFewSecondForTxUpdate": "取引履歴に取引が反映されるまで数秒お待ちください。", + "wallet_group": "ウォレットグループ", + "wallet_group_description_four": "まったく新しい種子の財布を作成します。", + "wallet_group_description_one": "ケーキウォレットでは、aを作成できます", + "wallet_group_description_three": "利用可能なウォレットおよび/またはウォレットグループの画面を表示します。または選択します", + "wallet_group_description_two": "既存のウォレットを選択して種子を共有します。各ウォレットグループには、各通貨タイプの単一のウォレットを含めることができます。\n\n選択できます", + "wallet_group_empty_state_text_one": "互換性のあるウォレットグループがないようです!\n\nタップ", + "wallet_group_empty_state_text_two": "以下に新しいものを作るために。", "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}", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index a5325ddff..29e68d4c3 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -130,6 +130,7 @@ "choose_one": "하나 선택", "choose_relay": "사용할 릴레이를 선택해주세요", "choose_wallet_currency": "지갑 통화를 선택하십시오:", + "choose_wallet_group": "지갑 그룹을 선택하십시오", "clear": "명확한", "clearnet_link": "클리어넷 링크", "close": "닫다", @@ -176,6 +177,7 @@ "create_invoice": "인보이스 생성", "create_new": "새 월렛 만들기", "create_new_account": "새 계정을 만들", + "create_new_seed": "새 씨앗을 만듭니다", "creating_new_wallet": "새 지갑 생성", "creating_new_wallet_error": "오류: ${description}", "creation_date": "생산 일", @@ -601,6 +603,8 @@ "seed_share": "시드 공유", "seed_title": "씨", "seedtype": "시드 타입", + "seedtype_alert_content": "다른 지갑과 씨앗을 공유하는 것은 BIP39 SeedType에서만 가능합니다.", + "seedtype_alert_title": "종자 경보", "seedtype_legacy": "레거시 (25 단어)", "seedtype_polyseed": "다문 (16 단어)", "select_backup_file": "백업 파일 선택", @@ -667,6 +671,7 @@ "setup_your_debit_card": "직불카드 설정", "share": "공유하다", "share_address": "주소 공유", + "shared_seed_wallet_groups": "공유 종자 지갑 그룹", "show_details": "세부정보 표시", "show_keys": "시드 / 키 표시", "show_market_place": "마켓플레이스 표시", @@ -691,6 +696,7 @@ "silent_payments_scanned_tip": "팁을 스캔했습니다! (${tip})", "silent_payments_scanning": "조용한 지불 스캔", "silent_payments_settings": "조용한 지불 설정", + "single_seed_wallets_group": "단일 씨앗 지갑", "slidable": "슬라이딩 가능", "sort_by": "정렬 기준", "spend_key_private": "지출 키 (은밀한)", @@ -850,8 +856,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "투표 중량", "waitFewSecondForTxUpdate": "거래 내역에 거래가 반영될 때까지 몇 초 정도 기다려 주세요.", + "wallet_group": "지갑 그룹", + "wallet_group_description_four": "완전히 새로운 씨앗으로 지갑을 만듭니다.", + "wallet_group_description_one": "케이크 지갑에서는 a를 만들 수 있습니다", + "wallet_group_description_three": "사용 가능한 지갑 및/또는 지갑 그룹 스크린을 볼 수 있습니다. 또는 선택하십시오", + "wallet_group_description_two": "씨앗을 공유 할 기존 지갑을 선택함으로써. 각 지갑 그룹은 각 통화 유형의 단일 지갑을 포함 할 수 있습니다. \n\n", + "wallet_group_empty_state_text_one": "호환 지갑 그룹이없는 것 같습니다 !\n\n TAP", + "wallet_group_empty_state_text_two": "아래에서 새로운 것을 만들기 위해.", "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}", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index d2332092a..d95a1b00d 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -130,6 +130,7 @@ "choose_one": "တစ်ခုရွေးပါ။", "choose_relay": "အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။", "choose_wallet_currency": "ပိုက်ဆံအိတ်ငွေကြေးကို ရွေးပါ-", + "choose_wallet_group": "ပိုက်ဆံအိတ်အုပ်စုရွေးပါ", "clear": "ရှင်းလင်းသော", "clearnet_link": "Clearnet လင့်ခ်", "close": "အနီးကပ်", @@ -176,6 +177,7 @@ "create_invoice": "ပြေစာဖန်တီးပါ။", "create_new": "Wallet အသစ်ဖန်တီးပါ။", "create_new_account": "အကောင့်အသစ်ဖန်တီးပါ။", + "create_new_seed": "မျိုးစေ့အသစ်ကိုဖန်တီးပါ", "creating_new_wallet": "ပိုက်ဆံအိတ်အသစ်ဖန်တီးခြင်း။", "creating_new_wallet_error": "အမှား- ${description}", "creation_date": "ဖန်တီးမှုနေ့စွဲ", @@ -600,6 +602,8 @@ "seed_share": "မျိုးစေ့မျှဝေပါ။", "seed_title": "မျိုးစေ့", "seedtype": "မျိုးပွားခြင်း", + "seedtype_alert_content": "အခြားပိုက်ဆံအိတ်များနှင့်မျိုးစေ့များကိုမျှဝေခြင်းသည် BIP39 sebyspe ဖြင့်သာဖြစ်သည်။", + "seedtype_alert_title": "ပျိုးပင်သတိပေးချက်", "seedtype_legacy": "အမွေအနှစ် (စကားလုံး 25 လုံး)", "seedtype_polyseed": "polyseed (စကားလုံး 16 လုံး)", "select_backup_file": "အရန်ဖိုင်ကို ရွေးပါ။", @@ -666,6 +670,7 @@ "setup_your_debit_card": "သင့်ဒက်ဘစ်ကတ်ကို စနစ်ထည့်သွင်းပါ။", "share": "မျှဝေပါ။", "share_address": "လိပ်စာမျှဝေပါ။", + "shared_seed_wallet_groups": "shared မျိုးစေ့ပိုက်ဆံအိတ်အုပ်စုများ", "show_details": "အသေးစိတ်ပြ", "show_keys": "မျိုးစေ့ /သော့များကို ပြပါ။", "show_market_place": "စျေးကွက်ကိုပြသပါ။", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "အစွန်အဖျားမှ scan ဖတ်! (${tip})", "silent_payments_scanning": "အသံတိတ်ငွေပေးချေမှု scanning", "silent_payments_settings": "အသံတိတ်ငွေပေးချေမှုဆက်တင်များ", + "single_seed_wallets_group": "တစ်ခုတည်းမျိုးစေ့ပိုက်ဆံအိတ်", "slidable": "လျှောချနိုင်သည်။", "sort_by": "အလိုက်စဥ်သည်", "spend_key_private": "သော့သုံးရန် (သီးသန့်)", @@ -849,8 +855,16 @@ "view_transaction_on": "ငွေလွှဲခြင်းကို ဖွင့်ကြည့်ပါ။", "voting_weight": "မဲပေးအလေးချိန်", "waitFewSecondForTxUpdate": "ငွေပေးငွေယူ မှတ်တမ်းတွင် ရောင်ပြန်ဟပ်ရန် စက္ကန့်အနည်းငယ်စောင့်ပါ။", + "wallet_group": "ပိုက်ဆံအိတ်အုပ်စု", + "wallet_group_description_four": "လုံးဝအသစ်သောမျိုးစေ့နှင့်အတူပိုက်ဆံအိတ်ဖန်တီးရန်။", + "wallet_group_description_one": "ကိတ်မုန့်၌, သင်တစ် ဦး ဖန်တီးနိုင်ပါတယ်", + "wallet_group_description_three": "ရရှိနိုင်သည့်ပိုက်ဆံအိတ်နှင့် / သို့မဟုတ်ပိုက်ဆံအိတ်အုပ်စုများမြင်ကွင်းကိုကြည့်ရှုရန်။ သို့မဟုတ်ရွေးချယ်ပါ", + "wallet_group_description_two": "နှင့်အတူမျိုးစေ့ဝေမျှဖို့ရှိပြီးသားပိုက်ဆံအိတ်တစ်ခုရွေးချယ်ခြင်းအားဖြင့်။ ပိုက်ဆံအိတ်အုပ်စုတစ်ခုစီတွင်ငွေကြေးအမျိုးအစားတစ်ခုစီ၏တစ်ခုတည်းသောပိုက်ဆံအိတ်တစ်ခုပါ 0 င်နိုင်သည်။ \n\n သင်ရွေးချယ်နိုင်သည်", + "wallet_group_empty_state_text_one": "သင့်တွင်သဟဇာတဖြစ်သောပိုက်ဆံအိတ်အုပ်စုများမရှိပါ။ !\n\n ကိုအသာပုတ်ပါ", + "wallet_group_empty_state_text_two": "အသစ်တစ်ခုကိုတစ်ခုလုပ်ဖို့အောက်တွင်ဖော်ပြထားသော။", "wallet_keys": "ပိုက်ဆံအိတ် အစေ့/သော့များ", "wallet_list_create_new_wallet": "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}", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index fcbf0f7c9..2bb505f3c 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -130,6 +130,7 @@ "choose_one": "Kies er een", "choose_relay": "Kies een relais dat u wilt gebruiken", "choose_wallet_currency": "Kies een portemonnee-valuta:", + "choose_wallet_group": "Kies portemonnee groep", "clear": "Duidelijk", "clearnet_link": "Clearnet-link", "close": "Dichtbij", @@ -176,6 +177,7 @@ "create_invoice": "Factuur maken", "create_new": "Maak een nieuwe portemonnee", "create_new_account": "Creëer een nieuw account", + "create_new_seed": "Maak nieuw zaadje", "creating_new_wallet": "Nieuwe portemonnee aanmaken", "creating_new_wallet_error": "Fout: ${description}", "creation_date": "Aanmaakdatum", @@ -600,6 +602,8 @@ "seed_share": "Deel zaad", "seed_title": "Zaad", "seedtype": "Zaadtype", + "seedtype_alert_content": "Het delen van zaden met andere portefeuilles is alleen mogelijk met BIP39 SeedType.", + "seedtype_alert_title": "Zaadtype alert", "seedtype_legacy": "Legacy (25 woorden)", "seedtype_polyseed": "Polyseed (16 woorden)", "select_backup_file": "Selecteer een back-upbestand", @@ -666,6 +670,7 @@ "setup_your_debit_card": "Stel uw debetkaart in", "share": "Deel", "share_address": "Deel adres", + "shared_seed_wallet_groups": "Gedeelde zaadportelgroepen", "show_details": "Toon details", "show_keys": "Toon zaad/sleutels", "show_market_place": "Toon Marktplaats", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "Gescand om te fooien! (${tip})", "silent_payments_scanning": "Stille betalingen scannen", "silent_payments_settings": "Stille betalingsinstellingen", + "single_seed_wallets_group": "Enkele zaadportefeuilles", "slidable": "Verschuifbaar", "sort_by": "Sorteer op", "spend_key_private": "Sleutel uitgeven (privaat)", @@ -850,8 +856,16 @@ "voting_weight": "Stemgewicht", "waitFewSecondForTxUpdate": "Wacht een paar seconden totdat de transactie wordt weergegeven in de transactiegeschiedenis", "waiting_payment_confirmation": "In afwachting van betalingsbevestiging", + "wallet_group": "Portemonnee", + "wallet_group_description_four": "om een ​​portemonnee te maken met een geheel nieuw zaadje.", + "wallet_group_description_one": "In cakeballet kun je een", + "wallet_group_description_three": "Om de beschikbare portefeuilles en/of portefeuillegroepen te zien. Of kies", + "wallet_group_description_two": "Door een bestaande portemonnee te selecteren om een ​​zaadje mee te delen. Elke portemonnee -groep kan een enkele portemonnee van elk valutietype bevatten. \n\n U kunt selecteren", + "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_keys": "Portemonnee zaad/sleutels", "wallet_list_create_new_wallet": "Maak een nieuwe portemonnee", + "wallet_list_edit_group_name": "Groepsnaam bewerken", "wallet_list_edit_wallet": "Portemonnee bewerken", "wallet_list_failed_to_load": "Laden mislukt ${wallet_name} portemonnee. ${error}", "wallet_list_failed_to_remove": "Verwijderen mislukt ${wallet_name} portemonnee. ${error}", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 13976991a..09d42e74a 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -130,6 +130,7 @@ "choose_one": "Wybierz jeden", "choose_relay": "Wybierz przekaźnik, którego chcesz użyć", "choose_wallet_currency": "Wybierz walutę portfela:", + "choose_wallet_group": "Wybierz grupę portfela", "clear": "Wyczyść", "clearnet_link": "łącze Clearnet", "close": "Zamknąć", @@ -176,6 +177,7 @@ "create_invoice": "Wystaw fakturę", "create_new": "Utwórz nowy portfel", "create_new_account": "Stwórz nowe konto", + "create_new_seed": "Utwórz nowe ziarno", "creating_new_wallet": "Tworzenie nowego portfela", "creating_new_wallet_error": "Błąd: ${description}", "creation_date": "Data utworzenia", @@ -600,6 +602,8 @@ "seed_share": "Udostępnij seed", "seed_title": "Seed", "seedtype": "Sedtype", + "seedtype_alert_content": "Dzielenie się nasionami z innymi portfelami jest możliwe tylko z BIP39 sededType.", + "seedtype_alert_title": "Ustanowienie typu sedype", "seedtype_legacy": "Dziedzictwo (25 słów)", "seedtype_polyseed": "Poliqueed (16 słów)", "select_backup_file": "Wybierz plik kopii zapasowej", @@ -666,6 +670,7 @@ "setup_your_debit_card": "Skonfiguruj swoją kartę debetową", "share": "Udział", "share_address": "Udostępnij adres", + "shared_seed_wallet_groups": "Wspólne grupy portfeli nasion", "show_details": "Pokaż szczegóły", "show_keys": "Pokaż seed/klucze", "show_market_place": "Pokaż rynek", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "Zeskanowany do napiwku! (${tip})", "silent_payments_scanning": "Skanowanie cichych płatności", "silent_payments_settings": "Ustawienia o cichej płatności", + "single_seed_wallets_group": "Pojedyncze portfele nasion", "slidable": "Przesuwne", "sort_by": "Sortuj według", "spend_key_private": "Klucz prywatny", @@ -849,8 +855,16 @@ "view_transaction_on": "Zobacz transakcje na ", "voting_weight": "Waga głosu", "waitFewSecondForTxUpdate": "Poczekaj kilka sekund, aż transakcja zostanie odzwierciedlona w historii transakcji", + "wallet_group": "Grupa portfela", + "wallet_group_description_four": "Aby stworzyć portfel z zupełnie nowym ziarnem.", + "wallet_group_description_one": "W portfelu ciasta możesz stworzyć", + "wallet_group_description_three": "Aby zobaczyć dostępny ekran portfeli i/lub grup portfeli. Lub wybierz", + "wallet_group_description_two": "Wybierając istniejący portfel do podzielenia nasion. Każda grupa portfela może zawierać pojedynczy portfel każdego typu waluty. \n\n możesz wybrać", + "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_keys": "Klucze portfela", "wallet_list_create_new_wallet": "Utwórz nowy portfel", + "wallet_list_edit_group_name": "Edytuj nazwę grupy", "wallet_list_edit_wallet": "Edytuj portfel", "wallet_list_failed_to_load": "Nie udało się załadować ${wallet_name} portfel. ${error}", "wallet_list_failed_to_remove": "Nie udało się usunąć ${wallet_name} portfel. ${error}", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 6f0f4f0b0..332623c04 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -130,6 +130,7 @@ "choose_one": "Escolha um", "choose_relay": "Escolha um relé para usar", "choose_wallet_currency": "Escolha a moeda da carteira:", + "choose_wallet_group": "Escolha o Grupo de Wallet", "clear": "Limpar", "clearnet_link": "link clear net", "close": "Fechar", @@ -176,6 +177,7 @@ "create_invoice": "Criar recibo", "create_new": "Criar nova carteira", "create_new_account": "Criar nova conta", + "create_new_seed": "Crie nova semente", "creating_new_wallet": "Criando nova carteira", "creating_new_wallet_error": "Erro: ${description}", "creation_date": "Data de criação", @@ -602,6 +604,8 @@ "seed_share": "Compartilhar semente", "seed_title": "Semente", "seedtype": "SeedType", + "seedtype_alert_content": "Compartilhar sementes com outras carteiras só é possível com o BIP39 SeedType.", + "seedtype_alert_title": "Alerta de SeedType", "seedtype_legacy": "Legado (25 palavras)", "seedtype_polyseed": "Polyseed (16 palavras)", "select_backup_file": "Selecione o arquivo de backup", @@ -668,6 +672,7 @@ "setup_your_debit_card": "Configure seu cartão de débito", "share": "Compartilhar", "share_address": "Compartilhar endereço", + "shared_seed_wallet_groups": "Grupos de carteira de sementes compartilhados", "show_details": "Mostrar detalhes", "show_keys": "Mostrar semente/chaves", "show_market_place": "Mostrar mercado", @@ -692,6 +697,7 @@ "silent_payments_scanned_tip": "Escaneado até o fim! (${tip})", "silent_payments_scanning": "Escanear Pagamentos Silenciosos", "silent_payments_settings": "Configurações de pagamentos silenciosos", + "single_seed_wallets_group": "Carteiras de sementes únicas", "slidable": "Deslizável", "sort_by": "Ordenar por", "spend_key_private": "Chave de gastos (privada)", @@ -852,8 +858,16 @@ "voting_weight": "Peso de votação", "waitFewSecondForTxUpdate": "Aguarde alguns segundos para que a transação seja refletida no histórico de transações", "waiting_payment_confirmation": "Aguardando confirmação de pagamento", + "wallet_group": "Grupo de carteira", + "wallet_group_description_four": "Para criar uma carteira com uma semente totalmente nova.", + "wallet_group_description_one": "Na carteira de bolo, você pode criar um", + "wallet_group_description_three": "Para ver as carteiras disponíveis e/ou os grupos de carteiras. Ou escolha", + "wallet_group_description_two": "Selecionando uma carteira existente para compartilhar uma semente. Cada grupo de carteira pode conter uma única carteira de cada tipo de moeda. \n\n você pode selecionar", + "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_keys": "Semente/chaves da carteira", "wallet_list_create_new_wallet": "Criar nova carteira", + "wallet_list_edit_group_name": "Editar o nome do grupo", "wallet_list_edit_wallet": "Editar carteira", "wallet_list_failed_to_load": "Falha ao abrir a carteira ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Falha ao remover a carteira ${wallet_name}. ${error}", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 5d600c3b4..5b64feb64 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -130,6 +130,7 @@ "choose_one": "Выбери один", "choose_relay": "Пожалуйста, выберите реле для использования", "choose_wallet_currency": "Пожалуйста, выберите валюту кошелька:", + "choose_wallet_group": "Выберите группу кошелька", "clear": "Очистить", "clearnet_link": "Клирнет ссылка", "close": "Закрывать", @@ -176,6 +177,7 @@ "create_invoice": "Создать счет", "create_new": "Создать новый кошелёк", "create_new_account": "Создать новый аккаунт", + "create_new_seed": "Создать новое семя", "creating_new_wallet": "Создание нового кошелька", "creating_new_wallet_error": "Ошибка: ${description}", "creation_date": "Дата создания", @@ -601,6 +603,8 @@ "seed_share": "Поделиться мнемонической фразой", "seed_title": "Мнемоническая фраза", "seedtype": "SEEDTYPE", + "seedtype_alert_content": "Обмен семенами с другими кошельками возможно только с BIP39 SeedType.", + "seedtype_alert_title": "SEEDTYPE ALERT", "seedtype_legacy": "Наследие (25 слов)", "seedtype_polyseed": "Полиса (16 слов)", "select_backup_file": "Выберите файл резервной копии", @@ -667,6 +671,7 @@ "setup_your_debit_card": "Настройте свою дебетовую карту", "share": "Делиться", "share_address": "Поделиться адресом", + "shared_seed_wallet_groups": "Общие группы кошелька семян", "show_details": "Показать детали", "show_keys": "Показать мнемоническую фразу/ключи", "show_market_place": "Показать торговую площадку", @@ -691,6 +696,7 @@ "silent_payments_scanned_tip": "Оканируется, чтобы чаевые! (${tip})", "silent_payments_scanning": "Сканирование безмолвных платежей", "silent_payments_settings": "Silent Payments Settings", + "single_seed_wallets_group": "Одиночные кошельки", "slidable": "Скользящий", "sort_by": "Сортировать по", "spend_key_private": "Приватный ключ траты", @@ -850,8 +856,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Вес голоса", "waitFewSecondForTxUpdate": "Пожалуйста, подождите несколько секунд, чтобы транзакция отразилась в истории транзакций.", + "wallet_group": "Группа кошелька", + "wallet_group_description_four": "создать кошелек с совершенно новым семенем.", + "wallet_group_description_one": "В кошельке для торта вы можете создать", + "wallet_group_description_three": "Чтобы увидеть доступные кошельки и/или экраны групп кошельков. Или выберите", + "wallet_group_description_two": "выбирая существующий кошелек, чтобы поделиться семенами. Каждая группа кошелька может содержать один кошелек каждого типа валюты. \n\n Вы можете выбрать", + "wallet_group_empty_state_text_one": "Похоже, у вас нет никаких совместимых групп кошелька !\n\n tap", + "wallet_group_empty_state_text_two": "ниже, чтобы сделать новый.", "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}", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index f924c0d2b..3bd50af5d 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -130,6 +130,7 @@ "choose_one": "เลือกหนึ่งรายการ", "choose_relay": "กรุณาเลือกรีเลย์ที่จะใช้", "choose_wallet_currency": "โปรดเลือกสกุลเงินของกระเป๋า:", + "choose_wallet_group": "เลือกกลุ่มกระเป๋าเงิน", "clear": "ล้าง", "clearnet_link": "ลิงค์เคลียร์เน็ต", "close": "ปิด", @@ -176,6 +177,7 @@ "create_invoice": "สร้างใบแจ้งหนี้", "create_new": "สร้างกระเป๋าใหม่", "create_new_account": "สร้างบัญชีใหม่", + "create_new_seed": "สร้างเมล็ดพันธุ์ใหม่", "creating_new_wallet": "กำลังสร้างกระเป๋าใหม่", "creating_new_wallet_error": "ข้อผิดพลาด: ${description}", "creation_date": "วันที่สร้าง", @@ -600,6 +602,8 @@ "seed_share": "แบ่งปัน seed", "seed_title": "Seed", "seedtype": "เมล็ดพันธุ์", + "seedtype_alert_content": "การแบ่งปันเมล็ดกับกระเป๋าเงินอื่น ๆ เป็นไปได้เฉพาะกับ bip39 seedtype", + "seedtype_alert_title": "การแจ้งเตือน seedtype", "seedtype_legacy": "มรดก (25 คำ)", "seedtype_polyseed": "โพลีส (16 คำ)", "select_backup_file": "เลือกไฟล์สำรอง", @@ -666,6 +670,7 @@ "setup_your_debit_card": "ตั้งค่าบัตรเดบิตของคุณ", "share": "แบ่งปัน", "share_address": "แชร์ที่อยู่", + "shared_seed_wallet_groups": "กลุ่มกระเป๋าเงินที่ใช้ร่วมกัน", "show_details": "แสดงรายละเอียด", "show_keys": "แสดงซีด/คีย์", "show_market_place": "แสดงตลาดกลาง", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "สแกนไปที่ปลาย! (${tip})", "silent_payments_scanning": "การสแกนการชำระเงินแบบเงียบ", "silent_payments_settings": "การตั้งค่าการชำระเงินแบบเงียบ", + "single_seed_wallets_group": "กระเป๋าเงินเดียว", "slidable": "เลื่อนได้", "sort_by": "เรียงตาม", "spend_key_private": "คีย์จ่าย (ส่วนตัว)", @@ -849,8 +855,16 @@ "view_transaction_on": "ดูการทำธุรกรรมบน ", "voting_weight": "น้ำหนักโหวต", "waitFewSecondForTxUpdate": "กรุณารอสักครู่เพื่อให้ธุรกรรมปรากฏในประวัติการทำธุรกรรม", + "wallet_group": "กลุ่มกระเป๋าเงิน", + "wallet_group_description_four": "เพื่อสร้างกระเป๋าเงินที่มีเมล็ดพันธุ์ใหม่ทั้งหมด", + "wallet_group_description_one": "ในกระเป๋าเงินเค้กคุณสามารถสร้างไฟล์", + "wallet_group_description_three": "หากต้องการดูกระเป๋าเงินและ/หรือกลุ่มกระเป๋าเงินที่มีอยู่ หรือเลือก", + "wallet_group_description_two": "โดยการเลือกกระเป๋าเงินที่มีอยู่เพื่อแบ่งปันเมล็ดด้วย แต่ละกลุ่มกระเป๋าเงินสามารถมีกระเป๋าเงินเดียวของแต่ละประเภทสกุลเงิน \n\n คุณสามารถเลือกได้", + "wallet_group_empty_state_text_one": "ดูเหมือนว่าคุณจะไม่มีกลุ่มกระเป๋าเงินที่เข้ากันได้ !\n\n แตะ", + "wallet_group_empty_state_text_two": "ด้านล่างเพื่อสร้างใหม่", "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}", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 00a59b9a0..48914c1e5 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -129,8 +129,9 @@ "choose_from_available_options": "Pumili mula sa magagamit na mga pagpipilian:", "choose_one": "Pumili ng isa", "choose_relay": "Mangyaring pumili ng relay na gagamitin", - "choose_wallet_currency": "Mangyaring piliin ang pera ng wallet:", - "clear": "Burahin", + "choose_wallet_currency": "Mangyaring piliin ang Pera ng Wallet:", + "choose_wallet_group": "Piliin ang pangkat ng Wallet", + "clear": "Malinaw", "clearnet_link": "Link ng Clearnet", "close": "Isara", "coin_control": "Coin control (opsyonal)", @@ -176,6 +177,7 @@ "create_invoice": "Lumikha ng invoice", "create_new": "Lumikha ng Bagong Wallet", "create_new_account": "Lumikha ng bagong account", + "create_new_seed": "Lumikha ng bagong binhi", "creating_new_wallet": "Lumikha ng bagong wallet", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Petsa ng paglikha", @@ -469,13 +471,13 @@ "please_try_to_connect_to_another_node": "Pakisubukang kumonekta sa iba pang node", "please_wait": "Mangyaring maghintay", "polygonscan_history": "Kasaysayan ng PolygonScan", - "powered_by": "Pinapatakbo ng${title}", - "pre_seed_button_text": "Naiitindihan ko. Ipakita ang aking seed", - "pre_seed_description": "Sa susunod na pahina ay makikita mo ang isang serye ng ${words} na salita. Ito ang iyong natatangi at pribadong seed at ito ang tanging paraan upang mabawi ang iyong wallet kung sakaling mawala o hindi gumana. Responsibilidad mong isulat ito sa isang ligtas na lugar sa labas ng Cake Wallet app.", - "pre_seed_title": "MAHALAGA", + "powered_by": "Pinapagana ng ${title}", + "pre_seed_button_text": "Naiintindihan ko. Ipakita sa akin ang aking binhi", + "pre_seed_description": "Sa susunod na pahina makikita mo ang isang serye ng mga ${words} na mga salita. Ito ang iyong natatangi at pribadong binhi at ito ang tanging paraan upang mabawi ang iyong pitaka kung sakaling mawala o madepektong paggawa. Responsibilidad mong isulat ito at itago ito sa isang ligtas na lugar sa labas ng cake wallet app.", + "pre_seed_title": "Mahalaga", "prepaid_cards": "Mga Prepaid Card", - "prevent_screenshots": "Maiwasan ang mga screenshot at pag-record ng screen", - "privacy": "Pagkapribado", + "prevent_screenshots": "Maiwasan ang mga screenshot at pag -record ng screen", + "privacy": "Privacy", "privacy_policy": "Patakaran sa Pagkapribado", "privacy_settings": "Settings para sa pagsasa-pribado", "private_key": "Private key", @@ -600,6 +602,8 @@ "seed_share": "Ibahagi ang seed", "seed_title": "Seed", "seedtype": "Seed type", + "seedtype_alert_content": "Ang pagbabahagi ng mga buto sa iba pang mga pitaka ay posible lamang sa bip39 seedtype.", + "seedtype_alert_title": "Alerto ng Seedtype", "seedtype_legacy": "Legacy (25 na salita)", "seedtype_polyseed": "Polyseed (16 na salita)", "select_backup_file": "Piliin ang backup na file", @@ -666,6 +670,7 @@ "setup_your_debit_card": "I-set up ang iyong debit card", "share": "Ibahagi", "share_address": "Ibahagi ang address", + "shared_seed_wallet_groups": "Ibinahaging mga pangkat ng pitaka ng binhi", "show_details": "Ipakita ang mga detalye", "show_keys": "Ipakita ang mga seed/key", "show_market_place": "Ipakita ang Marketplace", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "Na-scan sa tip! (${tip})", "silent_payments_scanning": "Pag-scan ng tahimik na pagbabayad", "silent_payments_settings": "Mga setting ng tahimik na pagbabayad", + "single_seed_wallets_group": "Solong mga pitaka ng binhi", "slidable": "Slidable", "sort_by": "Pag-uri-uriin sa pamamagitan ng", "spend_key_private": "Spend key (private)", @@ -849,8 +855,16 @@ "view_transaction_on": "Tingnan ang transaksyon sa ", "voting_weight": "Bigat ng pagboto", "waitFewSecondForTxUpdate": "Mangyaring maghintay ng ilang segundo para makita ang transaksyon sa history ng mga transaksyon", + "wallet_group": "Group ng Wallet", + "wallet_group_description_four": "Upang lumikha ng isang pitaka na may ganap na bagong binhi.", + "wallet_group_description_one": "Sa cake wallet, maaari kang lumikha ng isang", + "wallet_group_description_three": "Upang makita ang magagamit na mga wallets at/o screen ng mga pangkat ng pitaka. O pumili", + "wallet_group_description_two": "Sa pamamagitan ng pagpili ng isang umiiral na pitaka upang magbahagi ng isang binhi. Ang bawat pangkat ng pitaka ay maaaring maglaman ng isang solong pitaka ng bawat uri ng pera.\n\nMaaari kang pumili", + "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_keys": "Wallet seed/keys", "wallet_list_create_new_wallet": "Lumikha ng bagong wallet", + "wallet_list_edit_group_name": "I -edit ang Pangalan ng Grupo", "wallet_list_edit_wallet": "I-edit ang wallet", "wallet_list_failed_to_load": "Nabigong na-load ang ${wallet_name} na wallet. ${error}", "wallet_list_failed_to_remove": "Nabigong alisin ang ${wallet_name} wallet. ${error}", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 700dd05b3..22f7c8253 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -130,6 +130,7 @@ "choose_one": "Birini seç", "choose_relay": "Lütfen kullanmak için bir röle seçin", "choose_wallet_currency": "Lütfen cüzdanın para birimini seç:", + "choose_wallet_group": "Cüzdan Grubu Seçin", "clear": "Temizle", "clearnet_link": "Net bağlantı", "close": "Kapalı", @@ -176,6 +177,7 @@ "create_invoice": "Fatura oluşturmak", "create_new": "Yeni Cüzdan Oluştur", "create_new_account": "Yeni hesap oluştur", + "create_new_seed": "Yeni Tohum Oluştur", "creating_new_wallet": "Cüzdan oluşturuluyor", "creating_new_wallet_error": "Hata: ${description}", "creation_date": "Oluşturulma tarihi", @@ -600,6 +602,8 @@ "seed_share": "Tohumu paylaş", "seed_title": "Tohum", "seedtype": "Tohum", + "seedtype_alert_content": "Tohumları diğer cüzdanlarla paylaşmak sadece BIP39 tohumu ile mümkündür.", + "seedtype_alert_title": "SeedType uyarısı", "seedtype_legacy": "Miras (25 kelime)", "seedtype_polyseed": "Polyseed (16 kelime)", "select_backup_file": "Yedek dosyası seç", @@ -666,6 +670,7 @@ "setup_your_debit_card": "Banka kartını ayarla", "share": "Paylaşmak", "share_address": "Adresi paylaş", + "shared_seed_wallet_groups": "Paylaşılan tohum cüzdan grupları", "show_details": "Detayları Göster", "show_keys": "Tohumları/anahtarları göster", "show_market_place": "Pazar Yerini Göster", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "Bahşiş için tarandı! (${tip})", "silent_payments_scanning": "Sessiz Ödemeler Taraması", "silent_payments_settings": "Sessiz Ödeme Ayarları", + "single_seed_wallets_group": "Tek tohum cüzdanları", "slidable": "kaydırılabilir", "sort_by": "Göre sırala", "spend_key_private": "Harcama anahtarı (özel)", @@ -849,8 +855,16 @@ "view_transaction_on": "İşlemi şurada görüntüle ", "voting_weight": "Oy kullanma", "waitFewSecondForTxUpdate": "İşlemin işlem geçmişine yansıması için lütfen birkaç saniye bekleyin", + "wallet_group": "Cüzdan grubu", + "wallet_group_description_four": "Tamamen yeni bir tohumla bir cüzdan oluşturmak için.", + "wallet_group_description_one": "Kek cüzdanında bir", + "wallet_group_description_three": "Mevcut cüzdan ve/veya cüzdan grupları ekranını görmek için. Veya seç", + "wallet_group_description_two": "Bir tohumu paylaşmak için mevcut bir cüzdan seçerek. Her cüzdan grubu, her para türünün tek bir cüzdanı içerebilir. \n\n Seçebilirsiniz", + "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_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", "wallet_list_edit_wallet": "Cüzdanı düzenle", "wallet_list_failed_to_load": "Failed to load ${wallet_name} wallet. ${error}", "wallet_list_failed_to_remove": "${wallet_name} cüzdanı yüklenirken hata oluştu. ${error}", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 8772f4ba3..969e46036 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -130,6 +130,7 @@ "choose_one": "Вибери один", "choose_relay": "Будь ласка, виберіть реле для використання", "choose_wallet_currency": "Будь ласка, виберіть валюту гаманця:", + "choose_wallet_group": "Виберіть групу гаманця", "clear": "Очистити", "clearnet_link": "Посилання Clearnet", "close": "Закрити", @@ -176,6 +177,7 @@ "create_invoice": "Створити рахунок-фактуру", "create_new": "Створити новий гаманець", "create_new_account": "Створити новий акаунт", + "create_new_seed": "Створіть нове насіння", "creating_new_wallet": "Створення нового гаманця", "creating_new_wallet_error": "Помилка: ${description}", "creation_date": "Дата створення", @@ -601,6 +603,8 @@ "seed_share": "Поділитися мнемонічною фразою", "seed_title": "Мнемонічна фраза", "seedtype": "Насіннєвий тип", + "seedtype_alert_content": "Спільний доступ до інших гаманців можливе лише за допомогою BIP39 Seedtype.", + "seedtype_alert_title": "Попередження насінника", "seedtype_legacy": "Спадщина (25 слів)", "seedtype_polyseed": "Полісей (16 слів)", "select_backup_file": "Виберіть файл резервної копії", @@ -667,6 +671,7 @@ "setup_your_debit_card": "Налаштуйте свою дебетову картку", "share": "Поділіться", "share_address": "Поділитися адресою", + "shared_seed_wallet_groups": "Спільні групи насіннєвих гаманців", "show_details": "Показати деталі", "show_keys": "Показати мнемонічну фразу/ключі", "show_market_place": "Відображати маркетплейс", @@ -691,6 +696,7 @@ "silent_payments_scanned_tip": "Сканований на підказку! (${tip})", "silent_payments_scanning": "Мовчазні платежі сканування", "silent_payments_settings": "Налаштування мовчазних платежів", + "single_seed_wallets_group": "Поодинокі насінні гаманці", "slidable": "Розсувний", "sort_by": "Сортувати за", "spend_key_private": "Приватний ключ витрати", @@ -850,8 +856,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Вага голосування", "waitFewSecondForTxUpdate": "Будь ласка, зачекайте кілька секунд, поки транзакція відобразиться в історії транзакцій", + "wallet_group": "Група гаманців", + "wallet_group_description_four": "створити гаманець з абсолютно новим насінням.", + "wallet_group_description_one": "У гаманці тортів ви можете створити a", + "wallet_group_description_three": "Щоб побачити наявні гаманці та/або екран групи гаманців. Або вибрати", + "wallet_group_description_two": "Вибираючи існуючий гаманець, щоб поділитися насінням. Кожна група гаманця може містити один гаманець кожного типу валюти. \n\n Ви можете вибрати", + "wallet_group_empty_state_text_one": "Схоже, у вас немає сумісних груп гаманця !\n\n Торкніться", + "wallet_group_empty_state_text_two": "нижче, щоб зробити новий.", "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}", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 9b713900e..6b0906c75 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -130,6 +130,7 @@ "choose_one": "ایک کا انتخاب کریں", "choose_relay": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ", "choose_wallet_currency": "براہ کرم والیٹ کرنسی کا انتخاب کریں:", + "choose_wallet_group": "پرس گروپ کا انتخاب کریں", "clear": "صاف", "clearnet_link": "کلیرنیٹ لنک", "close": "بند کریں", @@ -176,6 +177,7 @@ "create_invoice": "انوائس بنائیں", "create_new": "نیا والیٹ بنائیں", "create_new_account": "نیا اکاؤنٹ بنانے", + "create_new_seed": "نیا بیج بنائیں", "creating_new_wallet": "نیا پرس بنانا", "creating_new_wallet_error": "خرابی: ${description}", "creation_date": "بنانے کی تاریخ", @@ -602,6 +604,8 @@ "seed_share": "بیج بانٹیں۔", "seed_title": "بیج", "seedtype": "سیڈ ٹائپ", + "seedtype_alert_content": "دوسرے بٹوے کے ساتھ بیجوں کا اشتراک صرف BIP39 بیج ٹائپ کے ساتھ ہی ممکن ہے۔", + "seedtype_alert_title": "سیڈ ٹائپ الرٹ", "seedtype_legacy": "میراث (25 الفاظ)", "seedtype_polyseed": "پالیسیڈ (16 الفاظ)", "select_backup_file": "بیک اپ فائل کو منتخب کریں۔", @@ -668,6 +672,7 @@ "setup_your_debit_card": "اپنا ڈیبٹ کارڈ ترتیب دیں۔", "share": "بانٹیں", "share_address": "پتہ شیئر کریں۔", + "shared_seed_wallet_groups": "مشترکہ بیج پرس گروپ", "show_details": "تفصیلات دکھائیں", "show_keys": "بیج / چابیاں دکھائیں۔", "show_market_place": "بازار دکھائیں۔", @@ -692,6 +697,7 @@ "silent_payments_scanned_tip": "نوکنے کے لئے اسکین! (${tip})", "silent_payments_scanning": "خاموش ادائیگی اسکیننگ", "silent_payments_settings": "خاموش ادائیگی کی ترتیبات", + "single_seed_wallets_group": "سنگل بیج کے بٹوے", "slidable": "سلائیڈ ایبل", "sort_by": "ترتیب دیں", "spend_key_private": "خرچ کی کلید (نجی)", @@ -851,8 +857,16 @@ "view_transaction_on": "لین دین دیکھیں آن", "voting_weight": "ووٹ کا وزن", "waitFewSecondForTxUpdate": "۔ﮟﯾﺮﮐ ﺭﺎﻈﺘﻧﺍ ﺎﮐ ﮉﻨﮑﯿﺳ ﺪﻨﭼ ﻡﺮﮐ ﮦﺍﺮﺑ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﯽﺳﺎﮑﻋ ﯽﮐ ﻦﯾﺩ ﻦﯿﻟ ﮟﯿﻣ ﺦﯾﺭﺎﺗ ﯽﮐ ﻦ", + "wallet_group": "پرس گروپ", + "wallet_group_description_four": "مکمل طور پر نئے بیج کے ساتھ پرس بنانے کے ل.", + "wallet_group_description_one": "کیک پرس میں ، آپ بنا سکتے ہیں", + "wallet_group_description_three": "دستیاب بٹوے اور/یا پرس گروپوں کی اسکرین کو دیکھنے کے لئے۔ یا منتخب کریں", + "wallet_group_description_two": "بیج کے ساتھ بانٹنے کے لئے موجودہ پرس کا انتخاب کرکے۔ ہر بٹوے گروپ میں ہر کرنسی کی قسم کا ایک بٹوے شامل ہوسکتا ہے۔ \n\n آپ منتخب کرسکتے ہیں", + "wallet_group_empty_state_text_one": "ایسا لگتا ہے کہ آپ کے پاس کوئی مطابقت پذیر والیٹ گروپس نہیں ہیں !\n\n نل", + "wallet_group_empty_state_text_two": "ایک نیا بنانے کے لئے ذیل میں.", "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}", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 79c0fc519..cc99dcf97 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -130,6 +130,7 @@ "choose_one": "Chọn một", "choose_relay": "Vui lòng chọn một relay để sử dụng", "choose_wallet_currency": "Vui lòng chọn tiền tệ của ví:", + "choose_wallet_group": "Chọn nhóm ví", "clear": "Xóa", "clearnet_link": "Liên kết Clearnet", "close": "Đóng", @@ -175,6 +176,7 @@ "create_invoice": "Tạo hóa đơn", "create_new": "Tạo Ví Mới", "create_new_account": "Tạo tài khoản mới", + "create_new_seed": "Tạo hạt giống mới", "creating_new_wallet": "Đang tạo ví mới", "creating_new_wallet_error": "Lỗi: ${description}", "creation_date": "Ngày Tạo", @@ -844,6 +846,12 @@ "view_transaction_on": "Xem giao dịch trên", "voting_weight": "Trọng số bỏ phiếu", "waitFewSecondForTxUpdate": "Vui lòng đợi vài giây để giao dịch được phản ánh trong lịch sử giao dịch", + "wallet_group_description_four": "Để tạo ra một ví với một hạt giống hoàn toàn mới.", + "wallet_group_description_one": "Trong ví bánh, bạn có thể tạo", + "wallet_group_description_three": "Để xem ví trên ví và/hoặc màn hình nhóm ví. Hoặc chọn", + "wallet_group_description_two": "Bằng cách chọn một ví hiện có để chia sẻ một hạt giống với. Mỗi nhóm ví có thể chứa một ví của mỗi loại tiền tệ. \n\n Bạn có thể chọn", + "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_keys": "Hạt giống/khóa ví", "wallet_list_create_new_wallet": "Tạo ví mới", "wallet_list_edit_wallet": "Chỉnh sửa ví", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index fbea1ba84..aa7264b67 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -130,6 +130,7 @@ "choose_one": "Ẹ yàn kan", "choose_relay": "Jọwọ yan yii lati lo", "choose_wallet_currency": "Ẹ jọ̀wọ́, yàn irú owó ti àpamọ́wọ́ yín:", + "choose_wallet_group": "Yan ẹgbẹ ogiri", "clear": "Pa gbogbo nǹkan", "clearnet_link": "Kọja ilọ oke", "close": "sunmo", @@ -176,6 +177,7 @@ "create_invoice": "Ṣe iwe iwe", "create_new": "Dá àpamọ́wọ́ tuntun", "create_new_account": "Dá àkáǹtì títun", + "create_new_seed": "Ṣẹda irugbin tuntun", "creating_new_wallet": "Ń dá àpamọ́wọ́ títun", "creating_new_wallet_error": "Àṣìṣe: ${description}", "creation_date": "Ọjọ ẹda", @@ -601,6 +603,8 @@ "seed_share": "Pín hóró", "seed_title": "Hóró", "seedtype": "Irugbin-seetypu", + "seedtype_alert_content": "Pinpin awọn irugbin pẹlu awọn gedo miiran ṣee ṣe pẹlu Bip39 irugbin.", + "seedtype_alert_title": "Ṣajọpọ Seeytype", "seedtype_legacy": "Legacy (awọn ọrọ 25)", "seedtype_polyseed": "Polyseed (awọn ọrọ 16)", "select_backup_file": "Select backup file", @@ -667,6 +671,7 @@ "setup_your_debit_card": "Dá àwọn káàdì ìrajà yín", "share": "Pinpin", "share_address": "Pín àdírẹ́sì", + "shared_seed_wallet_groups": "Awọn ẹgbẹ ti a pin irugbin", "show_details": "Fi ìsọfúnni kékeré hàn", "show_keys": "Wo hóró / àwọn kọ́kọ́rọ́", "show_market_place": "Wa Sopọ Pataki", @@ -691,6 +696,7 @@ "silent_payments_scanned_tip": "Ṣayẹwo si sample! (${tip})", "silent_payments_scanning": "Awọn sisanwo ipalọlọ", "silent_payments_settings": "Awọn eto isanwo ti o dakẹ", + "single_seed_wallets_group": "Awọn Wowei Awọn gige", "slidable": "Slidable", "sort_by": "Sa pelu", "spend_key_private": "Kọ́kọ́rọ́ sísan (àdáni)", @@ -850,8 +856,16 @@ "view_transaction_on": "Wo pàṣípààrọ̀ lórí ", "voting_weight": "Idibo iwuwo", "waitFewSecondForTxUpdate": "Fi inurere duro fun awọn iṣeju diẹ fun idunadura lati ṣe afihan ninu itan-akọọlẹ iṣowo", + "wallet_group": "Ẹgbẹ apamọwọ", + "wallet_group_description_four": "Lati ṣẹda apamọwọ kan pẹlu irugbin tuntun tuntun.", + "wallet_group_description_one": "Ni apamọwọ akara oyinbo, o le ṣẹda a", + "wallet_group_description_three": "Lati wo awọn Woleti ti o wa ati / tabi Iboju Wallt. Tabi yan", + "wallet_group_description_two": "nipa yiyan apamọwọ ti o wa tẹlẹ lati pin irugbin kan pẹlu. Ẹgbẹ apamọwọ kọọkan le ni apamọwọ kan ti iru owo kọọkan. \n\n O le yan", + "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_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ẹ", "wallet_list_edit_wallet": "Ṣatunkọ apamọwọ", "wallet_list_failed_to_load": "Ti kùnà ṣí́ àpamọ́wọ́ ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Ti kùnà yọ ${wallet_name} àpamọ́wọ́ kúrò. ${error}", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 2c0abe5eb..0306f8ad6 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -130,6 +130,7 @@ "choose_one": "选一个", "choose_relay": "请选择要使用的继电器", "choose_wallet_currency": "请选择钱包货币:", + "choose_wallet_group": "选择钱包组", "clear": "清空", "clearnet_link": "明网链接", "close": "关闭", @@ -176,6 +177,7 @@ "create_invoice": "创建发票", "create_new": "创建新钱包", "create_new_account": "建立新账户", + "create_new_seed": "创建新种子", "creating_new_wallet": "创建新钱包", "creating_new_wallet_error": "错误: ${description}", "creation_date": "创建日期", @@ -600,6 +602,8 @@ "seed_share": "分享种子", "seed_title": "种子", "seedtype": "籽粒", + "seedtype_alert_content": "只有BIP39籽粒可以与其他钱包共享种子。", + "seedtype_alert_title": "籽粒警报", "seedtype_legacy": "遗产(25个单词)", "seedtype_polyseed": "多种物品(16个单词)", "select_backup_file": "选择备份文件", @@ -666,6 +670,7 @@ "setup_your_debit_card": "设置你的借记卡", "share": "分享", "share_address": "分享地址", + "shared_seed_wallet_groups": "共享种子钱包组", "show_details": "显示详细信息", "show_keys": "显示种子/密钥", "show_market_place": "显示市场", @@ -690,6 +695,7 @@ "silent_payments_scanned_tip": "扫描到小费! (${tip})", "silent_payments_scanning": "无声付款扫描", "silent_payments_settings": "无声付款设置", + "single_seed_wallets_group": "单个种子钱包", "slidable": "可滑动", "sort_by": "排序方式", "spend_key_private": "Spend 密钥 (私钥)", @@ -849,8 +855,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "投票权重", "waitFewSecondForTxUpdate": "请等待几秒钟,交易才会反映在交易历史记录中", + "wallet_group": "钱包组", + "wallet_group_description_four": "创建一个带有全新种子的钱包。", + "wallet_group_description_one": "在蛋糕钱包中,您可以创建一个", + "wallet_group_description_three": "查看可用的钱包和/或钱包组屏幕。或选择", + "wallet_group_description_two": "通过选择现有的钱包与种子共享。每个钱包组都可以包含每种货币类型的单个钱包。\n\n您可以选择", + "wallet_group_empty_state_text_one": "看起来您没有任何兼容的钱包组!\n\n tap", + "wallet_group_empty_state_text_two": "下面是一个新的。", "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}", diff --git a/tool/configure.dart b/tool/configure.dart index f77db8d1c..af1f591a1 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -151,7 +151,7 @@ abstract class Bitcoin { String? passphrase, }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); - WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase}); + WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic, String? parentAddress}); WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); List getWordList(); Map getWalletKeys(Object wallet); @@ -558,7 +558,7 @@ abstract class Wownero { required String language, required int height}); WalletCredentials createWowneroRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); - WalletCredentials createWowneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String password}); + WalletCredentials createWowneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String? password}); Map getKeys(Object wallet); Object createWowneroTransactionCreationCredentials({required List outputs, required TransactionPriority priority}); Object createWowneroTransactionCreationCredentialsRaw({required List outputs, required TransactionPriority priority}); @@ -833,7 +833,7 @@ import 'package:eth_sig_util/util/utils.dart'; abstract class Ethereum { List getEthereumWordList(String language); WalletService createEthereumWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); + WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress}); WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createEthereumRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createEthereumHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); @@ -937,7 +937,7 @@ import 'package:eth_sig_util/util/utils.dart'; abstract class Polygon { List getPolygonWordList(String language); WalletService createPolygonWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); + WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress}); WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createPolygonRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createPolygonHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); @@ -1024,7 +1024,7 @@ abstract class BitcoinCash { Box walletInfoSource, Box unspentCoinSource, bool isDirect); WalletCredentials createBitcoinCashNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password, String? passphrase}); + {required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic, String? parentAddress}); WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( {required String name, required String mnemonic, required String password, String? passphrase}); @@ -1106,6 +1106,9 @@ abstract class Nano { WalletCredentials createNanoNewWalletCredentials({ required String name, String? password, + String? mnemonic, + String? parentAddress, + WalletInfo? walletInfo, }); WalletCredentials createNanoRestoreWalletFromSeedCredentials({ @@ -1221,7 +1224,7 @@ abstract class Solana { List getSolanaWordList(String language); WalletService createSolanaWalletService(Box walletInfoSource, bool isDirect); WalletCredentials createSolanaNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password}); + {required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress,}); WalletCredentials createSolanaRestoreWalletFromSeedCredentials( {required String name, required String mnemonic, required String password}); WalletCredentials createSolanaRestoreWalletFromPrivateKey( @@ -1307,7 +1310,8 @@ import 'package:cw_tron/tron_wallet_service.dart'; abstract class Tron { List getTronWordList(String language); WalletService createTronWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); + WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, + String? parentAddress}); WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createTronRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); String getAddress(WalletBase wallet); From 32e119e24fcd1a78fbe397e065f54701dbc767f1 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 20 Sep 2024 23:57:43 +0300 Subject: [PATCH 084/142] rbf-fixes-recomended-fee-rate (#1684) * rbf fixes * Revert "rbf fixes" * fix replaced transactions * re-format electrum_wallet.dart [skip ci] * minor fixes [skip ci] --------- Co-authored-by: OmarHatem --- cw_bitcoin/lib/electrum_transaction_info.dart | 9 +++- cw_bitcoin/lib/electrum_wallet.dart | 19 +++++-- .../lib/pending_bitcoin_transaction.dart | 1 + cw_core/lib/transaction_info.dart | 1 + lib/bitcoin/cw_bitcoin.dart | 8 ++- lib/di.dart | 50 ++++++++++++------- lib/router.dart | 2 +- .../dashboard/pages/transactions_page.dart | 7 +-- .../transaction_details/rbf_details_page.dart | 11 ++-- .../transaction_details_page.dart | 2 +- .../dashboard/dashboard_view_model.dart | 10 ++++ .../transaction_details_view_model.dart | 27 ++++++---- tool/configure.dart | 3 +- 13 files changed, 105 insertions(+), 45 deletions(-) diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 8b52b7aca..f2ef251c7 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -35,6 +35,7 @@ class ElectrumTransactionInfo extends TransactionInfo { List? outputAddresses, required TransactionDirection direction, required bool isPending, + required bool isReplaced, required DateTime date, required int confirmations, String? to, @@ -50,6 +51,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.direction = direction; this.date = date; this.isPending = isPending; + this.isReplaced = isReplaced; this.confirmations = confirmations; this.to = to; } @@ -98,6 +100,7 @@ class ElectrumTransactionInfo extends TransactionInfo { id: id, height: height, isPending: false, + isReplaced: false, fee: fee, direction: direction, amount: amount, @@ -173,6 +176,7 @@ class ElectrumTransactionInfo extends TransactionInfo { id: bundle.originalTransaction.txId(), height: height, isPending: bundle.confirmations == 0, + isReplaced: false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, fee: fee, @@ -196,6 +200,7 @@ class ElectrumTransactionInfo extends TransactionInfo { direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), isPending: data['isPending'] as bool, + isReplaced: data['isReplaced'] as bool? ?? false, confirmations: data['confirmations'] as int, inputAddresses: inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(), @@ -238,6 +243,7 @@ class ElectrumTransactionInfo extends TransactionInfo { direction: direction, date: date, isPending: isPending, + isReplaced: isReplaced ?? false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, confirmations: info.confirmations); @@ -251,6 +257,7 @@ class ElectrumTransactionInfo extends TransactionInfo { m['direction'] = direction.index; m['date'] = date.millisecondsSinceEpoch; m['isPending'] = isPending; + m['isReplaced'] = isReplaced; m['confirmations'] = confirmations; m['fee'] = fee; m['to'] = to; @@ -262,6 +269,6 @@ class ElectrumTransactionInfo extends TransactionInfo { } String toString() { - return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)'; + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)'; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 5eb4bd85c..fee115650 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1400,14 +1400,16 @@ abstract class ElectrumWalletBase } } - Future canReplaceByFee(ElectrumTransactionInfo tx) async { + int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize(); + + Future canReplaceByFee(ElectrumTransactionInfo tx) async { try { final bundle = await getTransactionExpanded(hash: tx.txHash); _updateInputsAndOutputs(tx, bundle); - if (bundle.confirmations > 0) return false; - return bundle.originalTransaction.canReplaceByFee; + if (bundle.confirmations > 0) return null; + return bundle.originalTransaction.canReplaceByFee ? bundle.originalTransaction.toHex() : null; } catch (e) { - return false; + return null; } } @@ -1589,6 +1591,13 @@ abstract class ElectrumWalletBase hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), )..addListener((transaction) async { + transactionHistory.transactions.values.forEach((tx) { + if (tx.id == hash) { + tx.isReplaced = true; + tx.isPending = false; + transactionHistory.addOne(tx); + } + }); transactionHistory.addOne(transaction); await updateBalance(); }); @@ -2317,6 +2326,7 @@ Future startRefresh(ScanData scanData) async { fee: 0, direction: TransactionDirection.incoming, isPending: false, + isReplaced: false, date: scanData.network == BitcoinNetwork.mainnet ? getDateByBitcoinHeight(tweakHeight) : DateTime.now(), @@ -2424,6 +2434,7 @@ class EstimatedTxResult { final int fee; final int amount; final bool spendsSilentPayment; + // final bool sendsToSilentPayment; final bool hasChange; final bool isSendAll; diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 26ed3a4be..e90e2193a 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -110,6 +110,7 @@ class PendingBitcoinTransaction with PendingTransaction { direction: TransactionDirection.outgoing, date: DateTime.now(), isPending: true, + isReplaced: false, confirmations: 0, fee: fee); } diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index 971e4ecdb..9d0c968d8 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -18,6 +18,7 @@ abstract class TransactionInfo extends Object with Keyable { String? to; String? from; String? evmSignatureName; + bool? isReplaced; List? inputAddresses; List? outputAddresses; diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index f09547fae..d1869a05d 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -411,12 +411,18 @@ class CWBitcoin extends Bitcoin { } @override - Future canReplaceByFee(Object wallet, Object transactionInfo) async { + Future canReplaceByFee(Object wallet, Object transactionInfo) async { final bitcoinWallet = wallet as ElectrumWallet; final tx = transactionInfo as ElectrumTransactionInfo; return bitcoinWallet.canReplaceByFee(tx); } + @override + int getTransactionVSize(Object wallet, String transactionHex) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.transactionVSize(transactionHex); + } + @override Future isChangeSufficientForFee(Object wallet, String txId, String newFee) async { final bitcoinWallet = wallet as ElectrumWallet; diff --git a/lib/di.dart b/lib/di.dart index 63b47f5e1..0b98244e6 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1080,21 +1080,40 @@ Future setup({ param1: derivations, ))); - getIt.registerFactoryParam( - (TransactionInfo transactionInfo, _) { - final wallet = getIt.get().wallet!; - return TransactionDetailsViewModel( - transactionInfo: transactionInfo, - transactionDescriptionBox: _transactionDescriptionBox, - wallet: wallet, - settingsStore: getIt.get(), - sendViewModel: getIt.get()); - }); + getIt.registerFactoryParam, void>( + (params, _) { + final transactionInfo = params[0] as TransactionInfo; + final canReplaceByFee = params[1] as bool? ?? false; + final wallet = getIt.get().wallet!; + + return TransactionDetailsViewModel( + transactionInfo: transactionInfo, + transactionDescriptionBox: _transactionDescriptionBox, + wallet: wallet, + settingsStore: getIt.get(), + sendViewModel: getIt.get(), + canReplaceByFee: canReplaceByFee, + ); + } + ); getIt.registerFactoryParam( - (TransactionInfo transactionInfo, _) => TransactionDetailsPage( - transactionDetailsViewModel: - getIt.get(param1: transactionInfo))); + (TransactionInfo transactionInfo, _) => TransactionDetailsPage( + transactionDetailsViewModel: getIt.get( + param1: [transactionInfo, false]))); + + getIt.registerFactoryParam, void>( + (params, _) { + final transactionInfo = params[0] as TransactionInfo; + final txHex = params[1] as String; + return RBFDetailsPage( + transactionDetailsViewModel: getIt.get( + param1: [transactionInfo, true], + ), + rawTransaction: txHex, + ); + } + ); getIt.registerFactoryParam( (newWalletTypeArguments, _) { @@ -1265,11 +1284,6 @@ Future setup({ getIt.registerFactory(() => CakePayAccountPage(getIt.get())); - getIt.registerFactoryParam( - (TransactionInfo transactionInfo, _) => RBFDetailsPage( - transactionDetailsViewModel: - getIt.get(param1: transactionInfo))); - getIt.registerFactory(() => AnonPayApi( useTorOnly: getIt.get().exchangeStatus == ExchangeApiMode.torOnly, wallet: getIt.get().wallet!)); diff --git a/lib/router.dart b/lib/router.dart index 0f65b4a05..16eeefeb1 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -375,7 +375,7 @@ Route createRoute(RouteSettings settings) { case Routes.bumpFeePage: return CupertinoPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get(param1: settings.arguments as TransactionInfo)); + builder: (_) => getIt.get(param1: settings.arguments as List)); case Routes.newSubaddress: return CupertinoPageRoute( diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index a9a1213ce..2538824ed 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -84,10 +84,7 @@ class TransactionsPage extends StatelessWidget { } final transaction = item.transaction; - final transactionType = dashboardViewModel.type == WalletType.ethereum && - transaction.evmSignatureName == 'approval' - ? ' (${transaction.evmSignatureName})' - : ''; + final transactionType = dashboardViewModel.getTransactionType(transaction); return Observer( builder: (_) => TransactionRow( @@ -102,7 +99,7 @@ class TransactionsPage extends StatelessWidget { : item.formattedFiatAmount, isPending: transaction.isPending, title: - item.formattedTitle + item.formattedStatus + ' $transactionType', + item.formattedTitle + item.formattedStatus + transactionType, isReceivedSilentPayment: dashboardViewModel.type == WalletType.bitcoin && bitcoin!.txIsReceivedSilentPayment(transaction), diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index 3faec48a8..b117a0b68 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -24,19 +24,22 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; class RBFDetailsPage extends BasePage { - RBFDetailsPage({required this.transactionDetailsViewModel}); + RBFDetailsPage({required this.transactionDetailsViewModel, required this.rawTransaction}) { + transactionDetailsViewModel.addBumpFeesListItems( + transactionDetailsViewModel.transactionInfo, rawTransaction); + } @override String get title => S.current.bump_fee; final TransactionDetailsViewModel transactionDetailsViewModel; + final String rawTransaction; bool _effectsInstalled = false; @override Widget body(BuildContext context) { _setEffects(context); - return Column( children: [ Expanded( @@ -166,7 +169,9 @@ class RBFDetailsPage extends BasePage { actionRightButton: () async { Navigator.of(popupContext).pop(); await transactionDetailsViewModel.sendViewModel.commitTransaction(); - // transactionStatePopup(); + try { + Navigator.of(popupContext).pop(); + } catch (_) {} }, actionLeftButton: () => Navigator.of(popupContext).pop(), feeFiatAmount: diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index d06b935dd..1b088fc31 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -75,7 +75,7 @@ class TransactionDetailsPage extends BasePage { text: S.of(context).bump_fee, onTap: () async { Navigator.of(context).pushNamed(Routes.bumpFeePage, - arguments: transactionDetailsViewModel.transactionInfo); + arguments: [transactionDetailsViewModel.transactionInfo, transactionDetailsViewModel.rawTransaction]); }, ), ); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 9aeb3a46b..4c3a9e1ea 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -764,6 +764,16 @@ abstract class DashboardViewModelBase with Store { } } + String getTransactionType(TransactionInfo tx) { + if (wallet.type == WalletType.bitcoin) { + if (tx.isReplaced == true) return ' (replaced)'; + } + + if (wallet.type == WalletType.ethereum && tx.evmSignatureName == 'approval') + return ' (${tx.evmSignatureName})'; + return ''; + } + Future refreshDashboard() async { reconnect(); } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index e4f9c3786..d3aa2a841 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -36,7 +36,8 @@ abstract class TransactionDetailsViewModelBase with Store { required this.transactionDescriptionBox, required this.wallet, required this.settingsStore, - required this.sendViewModel}) + required this.sendViewModel, + this.canReplaceByFee = false}) : items = [], RBFListItems = [], newFee = 0, @@ -51,8 +52,7 @@ abstract class TransactionDetailsViewModelBase with Store { break; case WalletType.bitcoin: _addElectrumListItems(tx, dateFormat); - _addBumpFeesListItems(tx); - _checkForRBF(tx); + if(!canReplaceByFee)_checkForRBF(tx); break; case WalletType.litecoin: case WalletType.bitcoinCash: @@ -139,13 +139,11 @@ abstract class TransactionDetailsViewModelBase with Store { bool showRecipientAddress; bool isRecipientAddressShown; int newFee; + String? rawTransaction; TransactionPriority? transactionPriority; @observable - bool _canReplaceByFee = false; - - @computed - bool get canReplaceByFee => _canReplaceByFee /*&& transactionInfo.confirmations <= 0*/; + bool canReplaceByFee; String _explorerUrl(WalletType type, String txId) { switch (type) { @@ -347,7 +345,7 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } - void _addBumpFeesListItems(TransactionInfo tx) { + void addBumpFeesListItems(TransactionInfo tx, String rawTransaction) { transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium(); final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true) ? 1 @@ -361,6 +359,14 @@ abstract class TransactionDetailsViewModelBase with Store { RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0')); + if (transactionInfo.fee != null && rawTransaction.isNotEmpty) { + final size = bitcoin!.getTransactionVSize(wallet, rawTransaction); + final recommendedRate = (transactionInfo.fee! / size).round() + 1; + + RBFListItems.add( + StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte')); + } + final priorities = priorityForWalletType(wallet.type); final selectedItem = priorities.indexOf(sendViewModel.transactionPriority); final customItem = priorities @@ -429,8 +435,9 @@ abstract class TransactionDetailsViewModelBase with Store { Future _checkForRBF(TransactionInfo tx) async { if (wallet.type == WalletType.bitcoin && transactionInfo.direction == TransactionDirection.outgoing) { - if (await bitcoin!.canReplaceByFee(wallet, tx)) { - _canReplaceByFee = true; + rawTransaction = await bitcoin!.canReplaceByFee(wallet, tx); + if (rawTransaction != null) { + canReplaceByFee = true; } } } diff --git a/tool/configure.dart b/tool/configure.dart index af1f591a1..815c9e399 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -205,7 +205,8 @@ abstract class Bitcoin { bool isTestnet(Object wallet); Future replaceByFee(Object wallet, String transactionHash, String fee); - Future canReplaceByFee(Object wallet, Object tx); + Future canReplaceByFee(Object wallet, Object tx); + int getTransactionVSize(Object wallet, String txHex); Future isChangeSufficientForFee(Object wallet, String txId, String newFee); int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size}); int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, From 4adb81c4dcf8e13665f9da8e5fff98a6a72df909 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Sun, 22 Sep 2024 03:46:51 +0100 Subject: [PATCH 085/142] CW-727/728-Automated-Integrated-Tests (#1514) * feat: Integration tests setup and tests for Disclaimer, Welcome and Setup Pin Code pages * feat: Integration test flow from start to restoring a wallet successfully done * test: Dashboard view test and linking to flow * feat: Testing the Exchange flow section, selecting sending and receiving currencies * test: Successfully create an exchange section * feat: Implement flow up to sending section * test: Complete Exchange flow * fix dependency issue * test: Final cleanups * feat: Add CI to run automated integration tests withan android emulator * feat: Adjust Automated integration test CI to run on ubuntu 20.04-a * fix: Move integration test CI into PR test build CI * ci: Add automated test ci which is a streamlined replica of pr test build ci * ci: Re-add step to access branch name * ci: Add KVM * ci: Add filepath to trigger the test run from * ci: Add required key * ci: Add required key * ci: Add missing secret key * ci: Add missing secret key * ci: Add nano secrets to workflow * ci: Switch step to free space on runner * ci: Remove timeout from workflow * ci: Confirm impact that removing copy_monero_deps would have on entire workflow time * ci: Update CI and temporarily remove cache related to emulator * ci: Remove dynamic java version * ci: Temporarily switch CI * ci: Switch to 11.x jdk * ci: Temporarily switch CI * ci: Revert ubuntu version * ci: Add more api levels * ci: Add more target options * ci: Settled on stable emulator matrix options * ci: Add more target options * ci: Modify flow * ci: Streamline api levels to 28 and 29 * ci: One more trial * ci: Switch to flutter drive * ci: Reduce options * ci: Remove haven from test * ci: Check for solana in list * ci: Adjust amounts and currencies for exchange flow * ci: Set write response on failure to true * ci: Split ci to funds and non funds related tests * test: Test for Send flow scenario and minor restructuring for test folders and files * chore: cleanup * ci: Pause CI for now * ci: Pause CI for now * ci: Pause CI for now * Fix: Add keys back to currency amount textfield widget * fix: Switch variable name * fix: remove automation for now * test: Updating send page robot and also syncing branch with main --------- Co-authored-by: OmarHatem --- .github/workflows/pr_test_build_android.yml | 3 + .gitignore | 3 + cw_nano/pubspec.lock | 4 +- .../components/common_test_cases.dart | 96 +++++ .../components/common_test_constants.dart | 13 + .../components/common_test_flows.dart | 101 +++++ integration_test/funds_related_tests.dart | 84 ++++ integration_test/helpers/mocks.dart | 25 ++ integration_test/helpers/test_helpers.dart | 100 +++++ .../integration_response_data.json | 1 + integration_test/robots/auth_page_robot.dart | 30 ++ .../robots/dashboard_page_robot.dart | 75 ++++ .../robots/disclaimer_page_robot.dart | 39 ++ .../robots/exchange_confirm_page_robot.dart | 45 +++ .../robots/exchange_page_robot.dart | 330 ++++++++++++++++ .../robots/exchange_trade_page_robot.dart | 152 ++++++++ .../robots/new_wallet_type_page_robot.dart | 59 +++ .../robots/pin_code_widget_robot.dart | 38 ++ .../restore_from_seed_or_key_robot.dart | 89 +++++ .../robots/restore_options_page_robot.dart | 42 ++ integration_test/robots/send_page_robot.dart | 366 ++++++++++++++++++ .../robots/setup_pin_code_robot.dart | 28 ++ .../robots/welcome_page_robot.dart | 40 ++ .../test_suites/exchange_flow_test.dart | 59 +++ .../test_suites/send_flow_test.dart | 41 ++ ios/Podfile.lock | 9 + lib/main.dart | 14 +- lib/src/screens/dashboard/dashboard_page.dart | 6 + .../dashboard/widgets/action_button.dart | 16 +- .../dashboard/widgets/sync_indicator.dart | 6 +- .../screens/disclaimer/disclaimer_page.dart | 3 + lib/src/screens/exchange/exchange_page.dart | 5 + .../exchange/exchange_template_page.dart | 2 + .../exchange/widgets/currency_picker.dart | 3 +- .../exchange/widgets/exchange_card.dart | 116 +++--- .../exchange_trade/exchange_confirm_page.dart | 2 + .../exchange_trade/exchange_trade_page.dart | 11 +- .../exchange_trade/information_page.dart | 3 +- .../new_wallet/new_wallet_type_page.dart | 3 + .../new_wallet/widgets/select_button.dart | 1 + lib/src/screens/pin_code/pin_code_widget.dart | 1 + .../receive/widgets/currency_input_field.dart | 26 +- .../screens/restore/restore_options_page.dart | 20 +- .../wallet_restore_from_keys_form.dart | 3 + .../wallet_restore_from_seed_form.dart | 13 +- .../screens/restore/wallet_restore_page.dart | 2 + lib/src/screens/send/send_page.dart | 35 +- .../send/widgets/confirm_sending_alert.dart | 61 +-- lib/src/screens/send/widgets/send_card.dart | 27 +- .../setup_pin_code/setup_pin_code.dart | 1 + lib/src/screens/welcome/welcome_page.dart | 2 + lib/src/widgets/address_text_field.dart | 45 +-- lib/src/widgets/alert_close_button.dart | 7 +- lib/src/widgets/alert_with_one_action.dart | 8 +- lib/src/widgets/alert_with_two_actions.dart | 15 + lib/src/widgets/base_alert_dialog.dart | 9 + lib/src/widgets/base_text_form_field.dart | 3 +- lib/src/widgets/option_tile.dart | 3 +- lib/src/widgets/picker.dart | 35 +- lib/src/widgets/picker_wrapper_widget.dart | 17 +- lib/src/widgets/primary_button.dart | 160 ++++---- .../scollable_with_bottom_section.dart | 3 + lib/src/widgets/search_bar_widget.dart | 1 + lib/src/widgets/seed_widget.dart | 22 +- pubspec_base.yaml | 5 +- test_driver/integration_test.dart | 33 ++ tool/utils/secret_key.dart | 1 + 67 files changed, 2381 insertions(+), 240 deletions(-) create mode 100644 integration_test/components/common_test_cases.dart create mode 100644 integration_test/components/common_test_constants.dart create mode 100644 integration_test/components/common_test_flows.dart create mode 100644 integration_test/funds_related_tests.dart create mode 100644 integration_test/helpers/mocks.dart create mode 100644 integration_test/helpers/test_helpers.dart create mode 100644 integration_test/integration_response_data.json create mode 100644 integration_test/robots/auth_page_robot.dart create mode 100644 integration_test/robots/dashboard_page_robot.dart create mode 100644 integration_test/robots/disclaimer_page_robot.dart create mode 100644 integration_test/robots/exchange_confirm_page_robot.dart create mode 100644 integration_test/robots/exchange_page_robot.dart create mode 100644 integration_test/robots/exchange_trade_page_robot.dart create mode 100644 integration_test/robots/new_wallet_type_page_robot.dart create mode 100644 integration_test/robots/pin_code_widget_robot.dart create mode 100644 integration_test/robots/restore_from_seed_or_key_robot.dart create mode 100644 integration_test/robots/restore_options_page_robot.dart create mode 100644 integration_test/robots/send_page_robot.dart create mode 100644 integration_test/robots/setup_pin_code_robot.dart create mode 100644 integration_test/robots/welcome_page_robot.dart create mode 100644 integration_test/test_suites/exchange_flow_test.dart create mode 100644 integration_test/test_suites/send_flow_test.dart create mode 100644 test_driver/integration_test.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index ac2480cb0..e096545c2 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -13,6 +13,9 @@ on: jobs: PR_test_build: runs-on: ubuntu-20.04 + strategy: + matrix: + api-level: [29] env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/.gitignore b/.gitignore index 8336ca512..970241189 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,9 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png macos/Runner/Configs/AppInfo.xcconfig + +integration_test/playground.dart + # Monero.dart (Monero_C) scripts/monero_c # iOS generated framework bin diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index e641024f7..ad1885d8b 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -277,10 +277,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: diff --git a/integration_test/components/common_test_cases.dart b/integration_test/components/common_test_cases.dart new file mode 100644 index 000000000..2e2991804 --- /dev/null +++ b/integration_test/components/common_test_cases.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class CommonTestCases { + WidgetTester tester; + CommonTestCases(this.tester); + + Future isSpecificPage() async { + await tester.pumpAndSettle(); + hasType(); + } + + Future tapItemByKey(String key, {bool shouldPumpAndSettle = true}) async { + final widget = find.byKey(ValueKey(key)); + await tester.tap(widget); + shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump(); + } + + Future tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async { + await tester.tap(finder); + shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump(); + } + + void hasText(String text, {bool hasWidget = true}) { + final textWidget = find.text(text); + expect(textWidget, hasWidget ? findsOneWidget : findsNothing); + } + + void hasType() { + final typeWidget = find.byType(T); + expect(typeWidget, findsOneWidget); + } + + void hasValueKey(String key) { + final typeWidget = find.byKey(ValueKey(key)); + expect(typeWidget, findsOneWidget); + } + + Future swipePage({bool swipeRight = true}) async { + await tester.drag(find.byType(PageView), Offset(swipeRight ? -300 : 300, 0)); + await tester.pumpAndSettle(); + } + + Future swipeByPageKey({required String key, bool swipeRight = true}) async { + await tester.drag(find.byKey(ValueKey(key)), Offset(swipeRight ? -300 : 300, 0)); + await tester.pumpAndSettle(); + } + + Future goBack() async { + tester.printToConsole('Routing back to previous screen'); + final NavigatorState navigator = tester.state(find.byType(Navigator)); + navigator.pop(); + await tester.pumpAndSettle(); + } + + Future scrollUntilVisible(String childKey, String parentScrollableKey, + {double delta = 300}) async { + final scrollableWidget = find.descendant( + of: find.byKey(Key(parentScrollableKey)), + matching: find.byType(Scrollable), + ); + + final isAlreadyVisibile = isWidgetVisible(find.byKey(ValueKey(childKey))); + + if (isAlreadyVisibile) return; + + await tester.scrollUntilVisible( + find.byKey(ValueKey(childKey)), + delta, + scrollable: scrollableWidget, + ); + } + + bool isWidgetVisible(Finder finder) { + try { + final Element element = finder.evaluate().single; + final RenderBox renderBox = element.renderObject as RenderBox; + return renderBox.paintBounds + .shift(renderBox.localToGlobal(Offset.zero)) + .overlaps(tester.binding.renderViews.first.paintBounds); + } catch (e) { + return false; + } + } + + Future enterText(String text, String editableTextKey) async { + final editableTextWidget = find.byKey(ValueKey((editableTextKey))); + + await tester.enterText(editableTextWidget, text); + + await tester.pumpAndSettle(); + } + + Future defaultSleepTime({int seconds = 2}) async => + await Future.delayed(Duration(seconds: seconds)); +} diff --git a/integration_test/components/common_test_constants.dart b/integration_test/components/common_test_constants.dart new file mode 100644 index 000000000..d8381973e --- /dev/null +++ b/integration_test/components/common_test_constants.dart @@ -0,0 +1,13 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_type.dart'; + +class CommonTestConstants { + static final pin = [0, 8, 0, 1]; + static final String sendTestAmount = '0.00008'; + static final String exchangeTestAmount = '8'; + static final WalletType testWalletType = WalletType.solana; + static final String testWalletName = 'Integrated Testing Wallet'; + static final CryptoCurrency testReceiveCurrency = CryptoCurrency.sol; + static final CryptoCurrency testDepositCurrency = CryptoCurrency.usdtSol; + static final String testWalletAddress = 'An2Y2fsUYKfYvN1zF89GAqR1e6GUMBg3qA83Y5ZWDf8L'; +} diff --git a/integration_test/components/common_test_flows.dart b/integration_test/components/common_test_flows.dart new file mode 100644 index 000000000..807509de9 --- /dev/null +++ b/integration_test/components/common_test_flows.dart @@ -0,0 +1,101 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/main.dart' as app; + +import '../robots/disclaimer_page_robot.dart'; +import '../robots/new_wallet_type_page_robot.dart'; +import '../robots/restore_from_seed_or_key_robot.dart'; +import '../robots/restore_options_page_robot.dart'; +import '../robots/setup_pin_code_robot.dart'; +import '../robots/welcome_page_robot.dart'; +import 'common_test_cases.dart'; +import 'common_test_constants.dart'; + +class CommonTestFlows { + CommonTestFlows(this._tester) + : _commonTestCases = CommonTestCases(_tester), + _welcomePageRobot = WelcomePageRobot(_tester), + _setupPinCodeRobot = SetupPinCodeRobot(_tester), + _disclaimerPageRobot = DisclaimerPageRobot(_tester), + _newWalletTypePageRobot = NewWalletTypePageRobot(_tester), + _restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester), + _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester); + + final WidgetTester _tester; + final CommonTestCases _commonTestCases; + + final WelcomePageRobot _welcomePageRobot; + final SetupPinCodeRobot _setupPinCodeRobot; + final DisclaimerPageRobot _disclaimerPageRobot; + final NewWalletTypePageRobot _newWalletTypePageRobot; + final RestoreOptionsPageRobot _restoreOptionsPageRobot; + final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot; + + Future startAppFlow(Key key) async { + await app.main(topLevelKey: ValueKey('send_flow_test_app_key')); + + await _tester.pumpAndSettle(); + + // --------- Disclaimer Page ------------ + // Tap checkbox to accept disclaimer + await _disclaimerPageRobot.tapDisclaimerCheckbox(); + + // Tap accept button + await _disclaimerPageRobot.tapAcceptButton(); + } + + Future restoreWalletThroughSeedsFlow() async { + await _welcomeToRestoreFromSeedsPath(); + await _restoreFromSeeds(); + } + + Future restoreWalletThroughKeysFlow() async { + await _welcomeToRestoreFromSeedsPath(); + await _restoreFromKeys(); + } + + Future _welcomeToRestoreFromSeedsPath() async { + // --------- Welcome Page --------------- + await _welcomePageRobot.navigateToRestoreWalletPage(); + + // ----------- Restore Options Page ----------- + // Route to restore from seeds page to continue flow + await _restoreOptionsPageRobot.navigateToRestoreFromSeedsPage(); + + // ----------- SetupPinCode Page ------------- + // Confirm initial defaults - Widgets to be displayed etc + await _setupPinCodeRobot.isSetupPinCodePage(); + + await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, true); + await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, false); + await _setupPinCodeRobot.tapSuccessButton(); + + // ----------- NewWalletType Page ------------- + // Confirm scroll behaviour works properly + await _newWalletTypePageRobot + .findParticularWalletTypeInScrollableList(CommonTestConstants.testWalletType); + + // Select a wallet and route to next page + await _newWalletTypePageRobot.selectWalletType(CommonTestConstants.testWalletType); + await _newWalletTypePageRobot.onNextButtonPressed(); + } + + Future _restoreFromSeeds() async { + // ----------- RestoreFromSeedOrKeys Page ------------- + await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(secrets.solanaTestWalletSeeds); + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); + } + + Future _restoreFromKeys() async { + await _commonTestCases.swipePage(); + await _commonTestCases.defaultSleepTime(); + + await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); + + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(''); + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); + } +} diff --git a/integration_test/funds_related_tests.dart b/integration_test/funds_related_tests.dart new file mode 100644 index 000000000..9d97d47f8 --- /dev/null +++ b/integration_test/funds_related_tests.dart @@ -0,0 +1,84 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'components/common_test_constants.dart'; +import 'components/common_test_flows.dart'; +import 'robots/auth_page_robot.dart'; +import 'robots/dashboard_page_robot.dart'; +import 'robots/exchange_confirm_page_robot.dart'; +import 'robots/exchange_page_robot.dart'; +import 'robots/exchange_trade_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + DashboardPageRobot dashboardPageRobot; + ExchangePageRobot exchangePageRobot; + ExchangeConfirmPageRobot exchangeConfirmPageRobot; + AuthPageRobot authPageRobot; + ExchangeTradePageRobot exchangeTradePageRobot; + CommonTestFlows commonTestFlows; + + group('Startup Test', () { + testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL', + (tester) async { + authPageRobot = AuthPageRobot(tester); + exchangePageRobot = ExchangePageRobot(tester); + dashboardPageRobot = DashboardPageRobot(tester); + exchangeTradePageRobot = ExchangeTradePageRobot(tester); + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + + await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key')); + + await commonTestFlows.restoreWalletThroughSeedsFlow(); + + // ----------- RestoreFromSeedOrKeys Page ------------- + await dashboardPageRobot.navigateToExchangePage(); + + // ----------- Exchange Page ------------- + await exchangePageRobot.isExchangePage(); + exchangePageRobot.hasResetButton(); + await exchangePageRobot.displayBothExchangeCards(); + exchangePageRobot.confirmRightComponentsDisplayOnDepositExchangeCards(); + exchangePageRobot.confirmRightComponentsDisplayOnReceiveExchangeCards(); + + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.enterDepositRefundAddress( + depositAddress: CommonTestConstants.testWalletAddress); + + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + + await exchangePageRobot.onExchangeButtonPressed(); + + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); + + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + } + + // ----------- Exchange Confirm Page ------------- + await exchangeConfirmPageRobot.isExchangeConfirmPage(); + + exchangeConfirmPageRobot.confirmComponentsOfTradeDisplayProperly(); + await exchangeConfirmPageRobot.confirmCopyTradeIdToClipBoardWorksProperly(); + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); + + // ----------- Exchange Trade Page ------------- + await exchangeTradePageRobot.isExchangeTradePage(); + exchangeTradePageRobot.hasInformationDialog(); + await exchangeTradePageRobot.onGotItButtonPressed(); + + await exchangeTradePageRobot.onConfirmSendingButtonPressed(); + + await exchangeTradePageRobot.handleConfirmSendResult(); + + await exchangeTradePageRobot.onSendButtonOnConfirmSendingDialogPressed(); + }); + }); +} diff --git a/integration_test/helpers/mocks.dart b/integration_test/helpers/mocks.dart new file mode 100644 index 000000000..01259bcc8 --- /dev/null +++ b/integration_test/helpers/mocks.dart @@ -0,0 +1,25 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; +import 'package:hive/hive.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAppStore extends Mock implements AppStore{} +class MockAuthService extends Mock implements AuthService{} +class MockSettingsStore extends Mock implements SettingsStore {} +class MockAuthenticationStore extends Mock implements AuthenticationStore{} +class MockWalletListStore extends Mock implements WalletListStore{} + + + +class MockLinkViewModel extends Mock implements LinkViewModel {} + +class MockHiveInterface extends Mock implements HiveInterface {} + +class MockHiveBox extends Mock implements Box {} + +class MockSecureStorage extends Mock implements SecureStorage{} \ No newline at end of file diff --git a/integration_test/helpers/test_helpers.dart b/integration_test/helpers/test_helpers.dart new file mode 100644 index 000000000..979ff60ff --- /dev/null +++ b/integration_test/helpers/test_helpers.dart @@ -0,0 +1,100 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; +import 'package:hive/hive.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'mocks.dart'; + +class TestHelpers { + static void setup() { + // Fallback values can also be declared here + registerDependencies(); + } + + static void registerDependencies() { + getAndRegisterAppStore(); + getAndRegisterAuthService(); + getAndRegisterSettingsStore(); + getAndRegisterAuthenticationStore(); + getAndRegisterWalletListStore(); + + getAndRegisterLinkViewModel(); + getAndRegisterSecureStorage(); + getAndRegisterHiveInterface(); + } + + static MockSettingsStore getAndRegisterSettingsStore() { + _removeRegistrationIfExists(); + final service = MockSettingsStore(); + getIt.registerSingleton(service); + return service; + } + + static MockAppStore getAndRegisterAppStore() { + _removeRegistrationIfExists(); + final service = MockAppStore(); + final settingsStore = getAndRegisterSettingsStore(); + + when(() => service.settingsStore).thenAnswer((invocation) => settingsStore); + getIt.registerSingleton(service); + return service; + } + + static MockAuthService getAndRegisterAuthService() { + _removeRegistrationIfExists(); + final service = MockAuthService(); + getIt.registerSingleton(service); + return service; + } + + static MockAuthenticationStore getAndRegisterAuthenticationStore() { + _removeRegistrationIfExists(); + final service = MockAuthenticationStore(); + when(() => service.state).thenReturn(AuthenticationState.uninitialized); + getIt.registerSingleton(service); + return service; + } + + static MockWalletListStore getAndRegisterWalletListStore() { + _removeRegistrationIfExists(); + final service = MockWalletListStore(); + getIt.registerSingleton(service); + return service; + } + + static MockLinkViewModel getAndRegisterLinkViewModel() { + _removeRegistrationIfExists(); + final service = MockLinkViewModel(); + getIt.registerSingleton(service); + return service; + } + + static MockHiveInterface getAndRegisterHiveInterface() { + _removeRegistrationIfExists(); + final service = MockHiveInterface(); + final box = MockHiveBox(); + getIt.registerSingleton(service); + return service; + } + + static MockSecureStorage getAndRegisterSecureStorage() { + _removeRegistrationIfExists(); + final service = MockSecureStorage(); + getIt.registerSingleton(service); + return service; + } + + static void _removeRegistrationIfExists() { + if (getIt.isRegistered()) { + getIt.unregister(); + } + } + + static void tearDown() => getIt.reset(); +} diff --git a/integration_test/integration_response_data.json b/integration_test/integration_response_data.json new file mode 100644 index 000000000..ec747fa47 --- /dev/null +++ b/integration_test/integration_response_data.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/integration_test/robots/auth_page_robot.dart b/integration_test/robots/auth_page_robot.dart new file mode 100644 index 000000000..6358d4398 --- /dev/null +++ b/integration_test/robots/auth_page_robot.dart @@ -0,0 +1,30 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; +import 'pin_code_widget_robot.dart'; + +class AuthPageRobot extends PinCodeWidgetRobot { + AuthPageRobot(this.tester) + : commonTestCases = CommonTestCases(tester), + super(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + bool onAuthPage() { + final hasPinButtons = find.byKey(ValueKey('pin_code_button_3_key')); + final hasPin = hasPinButtons.tryEvaluate(); + return hasPin; + } + + Future isAuthPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.setup_pin); + } +} diff --git a/integration_test/robots/dashboard_page_robot.dart b/integration_test/robots/dashboard_page_robot.dart new file mode 100644 index 000000000..fc917c3b2 --- /dev/null +++ b/integration_test/robots/dashboard_page_robot.dart @@ -0,0 +1,75 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class DashboardPageRobot { + DashboardPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isDashboardPage() async { + await commonTestCases.isSpecificPage(); + } + + void confirmServiceUpdateButtonDisplays() { + commonTestCases.hasValueKey('dashboard_page_services_update_button_key'); + } + + void confirmSyncIndicatorButtonDisplays() { + commonTestCases.hasValueKey('dashboard_page_sync_indicator_button_key'); + } + + void confirmMenuButtonDisplays() { + commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key'); + } + + Future confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType type, + {bool isHaven = false}) async { + //Balance Page + final walletName = walletTypeToString(type); + final assetName = isHaven ? '$walletName Assets' : walletName; + commonTestCases.hasText(assetName); + + // Swipe to Cake features Page + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); + await commonTestCases.defaultSleepTime(); + commonTestCases.hasText('Cake ${S.current.features}'); + + // Swipe back to balance + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); + await commonTestCases.defaultSleepTime(); + + // Swipe to Transactions Page + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); + await commonTestCases.defaultSleepTime(); + commonTestCases.hasText(S.current.transactions); + + // Swipe back to balance + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); + await commonTestCases.defaultSleepTime(seconds: 5); + } + + Future navigateToBuyPage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.buy}_action_button_key'); + } + + Future navigateToSendPage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.send}_action_button_key'); + } + + Future navigateToSellPage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.sell}_action_button_key'); + } + + Future navigateToReceivePage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.receive}_action_button_key'); + } + + Future navigateToExchangePage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.exchange}_action_button_key'); + } +} diff --git a/integration_test/robots/disclaimer_page_robot.dart b/integration_test/robots/disclaimer_page_robot.dart new file mode 100644 index 000000000..18861fc29 --- /dev/null +++ b/integration_test/robots/disclaimer_page_robot.dart @@ -0,0 +1,39 @@ +import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class DisclaimerPageRobot { + DisclaimerPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isDisclaimerPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasCheckIcon(bool hasBeenTapped) { + // The checked Icon should not be available initially, until user taps the checkbox + final checkIcon = find.byKey(ValueKey('disclaimer_check_icon_key')); + expect(checkIcon, hasBeenTapped ? findsOneWidget : findsNothing); + } + + void hasDisclaimerCheckbox() { + final checkBox = find.byKey(ValueKey('disclaimer_check_key')); + expect(checkBox, findsOneWidget); + } + + Future tapDisclaimerCheckbox() async { + await commonTestCases.tapItemByKey('disclaimer_check_key'); + + await commonTestCases.defaultSleepTime(); + } + + Future tapAcceptButton() async { + await commonTestCases.tapItemByKey('disclaimer_accept_button_key'); + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/exchange_confirm_page_robot.dart b/integration_test/robots/exchange_confirm_page_robot.dart new file mode 100644 index 000000000..160fd9dfb --- /dev/null +++ b/integration_test/robots/exchange_confirm_page_robot.dart @@ -0,0 +1,45 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class ExchangeConfirmPageRobot { + ExchangeConfirmPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isExchangeConfirmPage() async { + await commonTestCases.isSpecificPage(); + } + + void confirmComponentsOfTradeDisplayProperly() { + final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage)); + final trade = exchangeConfirmPage.trade; + + commonTestCases.hasText(trade.id); + commonTestCases.hasText('${trade.provider.title} ${S.current.trade_id}'); + + commonTestCases.hasValueKey('exchange_confirm_page_saved_id_button_key'); + commonTestCases.hasValueKey('exchange_confirm_page_copy_to_clipboard_button_key'); + } + + Future confirmCopyTradeIdToClipBoardWorksProperly() async { + final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage)); + final trade = exchangeConfirmPage.trade; + + await commonTestCases.tapItemByKey('exchange_confirm_page_copy_to_clipboard_button_key'); + + ClipboardData? clipboardData = await Clipboard.getData('text/plain'); + + expect(clipboardData?.text, trade.id); + } + + Future onSavedTradeIdButtonPressed() async { + await tester.pumpAndSettle(); + await commonTestCases.defaultSleepTime(); + await commonTestCases.tapItemByKey('exchange_confirm_page_saved_id_button_key'); + } +} diff --git a/integration_test/robots/exchange_page_robot.dart b/integration_test/robots/exchange_page_robot.dart new file mode 100644 index 000000000..b439e4791 --- /dev/null +++ b/integration_test/robots/exchange_page_robot.dart @@ -0,0 +1,330 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class ExchangePageRobot { + ExchangePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isExchangePage() async { + await commonTestCases.isSpecificPage(); + await commonTestCases.defaultSleepTime(); + } + + void hasResetButton() { + commonTestCases.hasText(S.current.reset); + } + + void displaysPresentProviderPicker() { + commonTestCases.hasType(); + } + + Future displayBothExchangeCards() async { + final ExchangePage exchangeCard = tester.widget( + find.byType(ExchangePage), + ); + + final depositKey = exchangeCard.depositKey; + final receiveKey = exchangeCard.receiveKey; + + final depositExchangeCard = find.byKey(depositKey); + expect(depositExchangeCard, findsOneWidget); + + final receiveExchangeCard = find.byKey(receiveKey); + expect(receiveExchangeCard, findsOneWidget); + } + + void confirmRightComponentsDisplayOnDepositExchangeCards() { + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + final depositCardPrefix = 'deposit_exchange_card'; + + commonTestCases.hasValueKey('${depositCardPrefix}_title_key'); + commonTestCases.hasValueKey('${depositCardPrefix}_currency_picker_button_key'); + commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_text_key'); + commonTestCases.hasValueKey('${depositCardPrefix}_amount_textfield_key'); + + exchangePage.depositKey.currentState!.changeLimits(min: '0.1'); + + commonTestCases.hasValueKey('${depositCardPrefix}_min_limit_text_key'); + + final initialCurrency = exchangeViewModel.depositCurrency; + if (initialCurrency.tag != null) { + commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_tag_text_key'); + } + + if (exchangeViewModel.hasAllAmount) { + commonTestCases.hasValueKey('${depositCardPrefix}_send_all_button_key'); + } + + if (exchangeViewModel.isMoneroWallet) { + commonTestCases.hasValueKey('${depositCardPrefix}_address_book_button_key'); + } + + if (exchangeViewModel.isDepositAddressEnabled) { + commonTestCases.hasValueKey('${depositCardPrefix}_editable_address_textfield_key'); + } else { + commonTestCases.hasValueKey('${depositCardPrefix}_non_editable_address_textfield_key'); + commonTestCases.hasValueKey('${depositCardPrefix}_copy_refund_address_button_key'); + } + + // commonTestCases.hasValueKey('${depositCardPrefix}_max_limit_text_key'); + } + + void confirmRightComponentsDisplayOnReceiveExchangeCards() { + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + final receiveCardPrefix = 'receive_exchange_card'; + + commonTestCases.hasValueKey('${receiveCardPrefix}_title_key'); + commonTestCases.hasValueKey('${receiveCardPrefix}_currency_picker_button_key'); + commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_text_key'); + commonTestCases.hasValueKey('${receiveCardPrefix}_amount_textfield_key'); + commonTestCases.hasValueKey('${receiveCardPrefix}_min_limit_text_key'); + + final initialCurrency = exchangeViewModel.receiveCurrency; + if (initialCurrency.tag != null) { + commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_tag_text_key'); + } + + if (exchangeViewModel.hasAllAmount) { + commonTestCases.hasValueKey('${receiveCardPrefix}_send_all_button_key'); + } + + if (exchangeViewModel.isMoneroWallet) { + commonTestCases.hasValueKey('${receiveCardPrefix}_address_book_button_key'); + } + + commonTestCases.hasValueKey('${receiveCardPrefix}_editable_address_textfield_key'); + } + + Future selectDepositCurrency(CryptoCurrency depositCurrency) async { + final depositPrefix = 'deposit_exchange_card'; + final currencyPickerKey = '${depositPrefix}_currency_picker_button_key'; + final currencyPickerDialogKey = '${depositPrefix}_currency_picker_dialog_button_key'; + + await commonTestCases.tapItemByKey(currencyPickerKey); + commonTestCases.hasValueKey(currencyPickerDialogKey); + + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + + if (depositCurrency == exchangeViewModel.depositCurrency) { + await commonTestCases.defaultSleepTime(); + await commonTestCases + .tapItemByKey('picker_items_index_${depositCurrency.name}_selected_item_button_key'); + return; + } + + await commonTestCases.scrollUntilVisible( + 'picker_items_index_${depositCurrency.name}_button_key', + 'picker_scrollbar_key', + ); + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${depositCurrency.name}_button_key'); + } + + Future selectReceiveCurrency(CryptoCurrency receiveCurrency) async { + final receivePrefix = 'receive_exchange_card'; + final currencyPickerKey = '${receivePrefix}_currency_picker_button_key'; + final currencyPickerDialogKey = '${receivePrefix}_currency_picker_dialog_button_key'; + + await commonTestCases.tapItemByKey(currencyPickerKey); + commonTestCases.hasValueKey(currencyPickerDialogKey); + + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + + if (receiveCurrency == exchangeViewModel.receiveCurrency) { + await commonTestCases + .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key'); + return; + } + + await commonTestCases.scrollUntilVisible( + 'picker_items_index_${receiveCurrency.name}_button_key', + 'picker_scrollbar_key', + ); + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key'); + } + + Future enterDepositAmount(String amount) async { + await commonTestCases.enterText(amount, 'deposit_exchange_card_amount_textfield_key'); + } + + Future enterDepositRefundAddress({String? depositAddress}) async { + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + + if (exchangeViewModel.isDepositAddressEnabled && depositAddress != null) { + await commonTestCases.enterText( + depositAddress, 'deposit_exchange_card_editable_address_textfield_key'); + } + } + + Future enterReceiveAddress(String receiveAddress) async { + await commonTestCases.enterText( + receiveAddress, + 'receive_exchange_card_editable_address_textfield_key', + ); + await commonTestCases.defaultSleepTime(); + } + + Future onExchangeButtonPressed() async { + await commonTestCases.tapItemByKey('exchange_page_exchange_button_key'); + await commonTestCases.defaultSleepTime(); + } + + bool hasMaxLimitError() { + final maxErrorText = find.text(S.current.error_text_input_above_maximum_limit); + + bool hasMaxError = maxErrorText.tryEvaluate(); + + return hasMaxError; + } + + bool hasMinLimitError() { + final minErrorText = find.text(S.current.error_text_input_below_minimum_limit); + + bool hasMinError = minErrorText.tryEvaluate(); + + return hasMinError; + } + + bool hasTradeCreationFailureError() { + final tradeCreationFailureDialogButton = + find.byKey(ValueKey('exchange_page_trade_creation_failure_dialog_button_key')); + + bool hasTradeCreationFailure = tradeCreationFailureDialogButton.tryEvaluate(); + tester.printToConsole('Trade not created error: $hasTradeCreationFailure'); + return hasTradeCreationFailure; + } + + Future onTradeCreationFailureDialogButtonPressed() async { + await commonTestCases.tapItemByKey('exchange_page_trade_creation_failure_dialog_button_key'); + } + + /// Handling Trade Failure Errors or errors shown through the Failure Dialog. + /// + /// Simulating the user's flow and response when this error comes up. + /// Examples are: + /// - No provider can handle this trade error, + /// - Trade amount below limit error. + Future _handleTradeCreationFailureErrors() async { + bool isTradeCreationFailure = false; + + isTradeCreationFailure = hasTradeCreationFailureError(); + + int maxRetries = 20; + int retries = 0; + + while (isTradeCreationFailure && retries < maxRetries) { + await tester.pump(); + + await onTradeCreationFailureDialogButtonPressed(); + + await commonTestCases.defaultSleepTime(seconds: 5); + + await onExchangeButtonPressed(); + + isTradeCreationFailure = hasTradeCreationFailureError(); + retries++; + } + } + + /// Handles the min limit error. + /// + /// Simulates the user's flow and response when it comes up. + /// + /// Has a max retry of 20 times. + Future _handleMinLimitError(String initialAmount) async { + bool isMinLimitError = false; + + isMinLimitError = hasMinLimitError(); + + double amount; + + amount = double.parse(initialAmount); + + int maxRetries = 20; + int retries = 0; + + while (isMinLimitError && retries < maxRetries) { + amount++; + tester.printToConsole('Amount: $amount'); + + enterDepositAmount(amount.toString()); + + await commonTestCases.defaultSleepTime(); + + await onExchangeButtonPressed(); + + isMinLimitError = hasMinLimitError(); + + retries++; + } + + if (retries >= maxRetries) { + tester.printToConsole('Max retries reached for minLimit Error. Exiting loop.'); + } + } + + /// Handles the max limit error. + /// + /// Simulates the user's flow and response when it comes up. + /// + /// Has a max retry of 20 times. + Future _handleMaxLimitError(String initialAmount) async { + bool isMaxLimitError = false; + + isMaxLimitError = hasMaxLimitError(); + + double amount; + + amount = double.parse(initialAmount); + + int maxRetries = 20; + int retries = 0; + + while (isMaxLimitError && retries < maxRetries) { + amount++; + tester.printToConsole('Amount: $amount'); + + enterDepositAmount(amount.toString()); + + await commonTestCases.defaultSleepTime(); + + await onExchangeButtonPressed(); + + isMaxLimitError = hasMaxLimitError(); + + retries++; + } + + if (retries >= maxRetries) { + tester.printToConsole('Max retries reached for maxLimit Error. Exiting loop.'); + } + } + + Future handleErrors(String initialAmount) async { + await tester.pumpAndSettle(); + + await _handleMinLimitError(initialAmount); + + await _handleMaxLimitError(initialAmount); + + await _handleTradeCreationFailureErrors(); + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/exchange_trade_page_robot.dart b/integration_test/robots/exchange_trade_page_robot.dart new file mode 100644 index 000000000..5708b6fae --- /dev/null +++ b/integration_test/robots/exchange_trade_page_robot.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class ExchangeTradePageRobot { + ExchangeTradePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isExchangeTradePage() async { + await commonTestCases.isSpecificPage(); + } + + void hasInformationDialog() { + commonTestCases.hasValueKey('information_page_dialog_key'); + } + + Future onGotItButtonPressed() async { + await commonTestCases.tapItemByKey('information_page_got_it_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future onConfirmSendingButtonPressed() async { + tester.printToConsole('Now confirming sending'); + + await commonTestCases.tapItemByKey( + 'exchange_trade_page_confirm_sending_button_key', + shouldPumpAndSettle: false, + ); + + final Completer completer = Completer(); + + // Loop to wait for the async operation to complete + while (true) { + await Future.delayed(Duration(seconds: 1)); + + final ExchangeTradeState state = tester.state(find.byType(ExchangeTradeForm)); + final execState = state.widget.exchangeTradeViewModel.sendViewModel.state; + + bool isDone = execState is ExecutedSuccessfullyState; + bool isFailed = execState is FailureState; + + tester.printToConsole('isDone: $isDone'); + tester.printToConsole('isFailed: $isFailed'); + + if (isDone || isFailed) { + tester.printToConsole( + isDone ? 'Completer is done' : 'Completer is done though operation failed'); + completer.complete(); + await tester.pump(); + break; + } else { + tester.printToConsole('Completer is not done'); + await tester.pump(); + } + } + + await expectLater(completer.future, completes); + + tester.printToConsole('Done confirming sending'); + + await commonTestCases.defaultSleepTime(seconds: 4); + } + + Future onSendButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Send Button on Confirm Dialog Triggered'); + await commonTestCases.defaultSleepTime(seconds: 4); + + final sendText = find.text(S.current.send); + bool hasText = sendText.tryEvaluate(); + + if (hasText) { + await commonTestCases.tapItemByFinder(sendText); + + await commonTestCases.defaultSleepTime(seconds: 4); + } + } + + Future onCancelButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Cancel Button on Confirm Dialog Triggered'); + + await commonTestCases.tapItemByKey( + 'exchange_trade_page_confirm_sending_dialog_cancel_button_key', + ); + + await commonTestCases.defaultSleepTime(); + } + + Future onSendFailureDialogButtonPressed() async { + await commonTestCases.defaultSleepTime(seconds: 6); + + tester.printToConsole('Send Button Failure Dialog Triggered'); + + await commonTestCases.tapItemByKey('exchange_trade_page_send_failure_dialog_button_key'); + } + + Future hasErrorWhileSending() async { + await tester.pump(); + + tester.printToConsole('Checking if there is an error'); + + final errorDialog = find.byKey( + ValueKey('exchange_trade_page_send_failure_dialog_button_key'), + ); + + bool hasError = errorDialog.tryEvaluate(); + + tester.printToConsole('Has error: $hasError'); + + return hasError; + } + + Future handleConfirmSendResult() async { + bool hasError = false; + + hasError = await hasErrorWhileSending(); + + int maxRetries = 20; + int retries = 0; + + while (hasError && retries < maxRetries) { + tester.printToConsole('hasErrorInLoop: $hasError'); + await tester.pump(); + + await onSendFailureDialogButtonPressed(); + tester.printToConsole('Failure button tapped'); + + await commonTestCases.defaultSleepTime(); + + await onConfirmSendingButtonPressed(); + tester.printToConsole('Confirm sending button tapped'); + + hasError = await hasErrorWhileSending(); + + retries++; + } + + if (!hasError) { + tester.printToConsole('No error, proceeding with flow'); + await tester.pump(); + } + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/new_wallet_type_page_robot.dart b/integration_test/robots/new_wallet_type_page_robot.dart new file mode 100644 index 000000000..89fc8d390 --- /dev/null +++ b/integration_test/robots/new_wallet_type_page_robot.dart @@ -0,0 +1,59 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class NewWalletTypePageRobot { + NewWalletTypePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isNewWalletTypePage() async { + await commonTestCases.isSpecificPage(); + } + + void displaysCorrectTitle(bool isCreate) { + commonTestCases.hasText( + isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet, + ); + } + + void hasWalletTypeForm() { + commonTestCases.hasType(); + } + + void displaysCorrectImage(ThemeType type) { + final walletTypeImage = Image.asset('assets/images/wallet_type.png').image; + final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png').image; + + find.image( + type == ThemeType.dark ? walletTypeImage : walletTypeLightImage, + ); + } + + Future findParticularWalletTypeInScrollableList(WalletType type) async { + final scrollableWidget = find.descendant( + of: find.byKey(Key('new_wallet_type_scrollable_key')), + matching: find.byType(Scrollable), + ); + + await tester.scrollUntilVisible( + find.byKey(ValueKey('new_wallet_type_${type.name}_button_key')), + 300, + scrollable: scrollableWidget, + ); + } + + Future selectWalletType(WalletType type) async { + await commonTestCases.tapItemByKey('new_wallet_type_${type.name}_button_key'); + } + + Future onNextButtonPressed() async { + await commonTestCases.tapItemByKey('new_wallet_type_next_button_key'); + } +} diff --git a/integration_test/robots/pin_code_widget_robot.dart b/integration_test/robots/pin_code_widget_robot.dart new file mode 100644 index 000000000..b6805e9e0 --- /dev/null +++ b/integration_test/robots/pin_code_widget_robot.dart @@ -0,0 +1,38 @@ +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class PinCodeWidgetRobot { + PinCodeWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + void hasPinCodeWidget() { + final pinCodeWidget = find.bySubtype(); + expect(pinCodeWidget, findsOneWidget); + } + + void hasNumberButtonsVisible() { + // Confirmation for buttons 1-9 + for (var i = 1; i < 10; i++) { + commonTestCases.hasValueKey('pin_code_button_${i}_key'); + } + + // Confirmation for 0 button + commonTestCases.hasValueKey('pin_code_button_0_key'); + } + + Future pushPinButton(int index) async { + await commonTestCases.tapItemByKey('pin_code_button_${index}_key'); + } + + Future enterPinCode(List pinCode, bool isFirstEntry) async { + for (int pin in pinCode) { + await pushPinButton(pin); + } + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/restore_from_seed_or_key_robot.dart b/integration_test/robots/restore_from_seed_or_key_robot.dart new file mode 100644 index 000000000..43a65095d --- /dev/null +++ b/integration_test/robots/restore_from_seed_or_key_robot.dart @@ -0,0 +1,89 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; +import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class RestoreFromSeedOrKeysPageRobot { + RestoreFromSeedOrKeysPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isRestoreFromSeedKeyPage() async { + await commonTestCases.isSpecificPage(); + } + + Future confirmViewComponentsDisplayProperlyPerPageView() async { + commonTestCases.hasText(S.current.wallet_name); + commonTestCases.hasText(S.current.enter_seed_phrase); + commonTestCases.hasText(S.current.restore_title_from_seed); + + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_textfield_key'); + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key'); + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'); + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key'); + + commonTestCases.hasText(S.current.private_key, hasWidget: false); + commonTestCases.hasText(S.current.restore_title_from_keys, hasWidget: false); + + await commonTestCases.swipePage(); + await commonTestCases.defaultSleepTime(); + + commonTestCases.hasText(S.current.wallet_name); + commonTestCases.hasText(S.current.private_key); + commonTestCases.hasText(S.current.restore_title_from_keys); + + commonTestCases.hasText(S.current.enter_seed_phrase, hasWidget: false); + commonTestCases.hasText(S.current.restore_title_from_seed, hasWidget: false); + + await commonTestCases.swipePage(swipeRight: false); + } + + void confirmRestoreButtonDisplays() { + commonTestCases.hasValueKey('wallet_restore_seed_or_key_restore_button_key'); + } + + void confirmAdvancedSettingButtonDisplays() { + commonTestCases.hasValueKey('wallet_restore_advanced_settings_button_key'); + } + + Future enterWalletNameText(String walletName, {bool isSeedFormEntry = true}) async { + await commonTestCases.enterText( + walletName, + 'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_textfield_key', + ); + } + + Future selectWalletNameFromAvailableOptions({bool isSeedFormEntry = true}) async { + await commonTestCases.tapItemByKey( + 'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_refresh_button_key', + ); + } + + Future enterSeedPhraseForWalletRestore(String text) async { + ValidatableAnnotatedEditableTextState seedTextState = + await tester.state(find.byType(ValidatableAnnotatedEditableText)); + + seedTextState.widget.controller.text = text; + await tester.pumpAndSettle(); + } + + Future onPasteSeedPhraseButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'); + } + + Future enterPrivateKeyForWalletRestore(String privateKey) async { + await commonTestCases.enterText( + privateKey, + 'wallet_restore_from_key_private_key_textfield_key', + ); + await tester.pumpAndSettle(); + } + + Future onRestoreWalletButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/restore_options_page_robot.dart b/integration_test/robots/restore_options_page_robot.dart new file mode 100644 index 000000000..b3cefc90c --- /dev/null +++ b/integration_test/robots/restore_options_page_robot.dart @@ -0,0 +1,42 @@ +import 'package:cake_wallet/src/screens/restore/restore_options_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class RestoreOptionsPageRobot { + RestoreOptionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isRestoreOptionsPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasRestoreOptionsButton() { + commonTestCases.hasValueKey('restore_options_from_seeds_button_key'); + commonTestCases.hasValueKey('restore_options_from_backup_button_key'); + commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key'); + commonTestCases.hasValueKey('restore_options_from_qr_button_key'); + } + + Future navigateToRestoreFromSeedsPage() async { + await commonTestCases.tapItemByKey('restore_options_from_seeds_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToRestoreFromBackupPage() async { + await commonTestCases.tapItemByKey('restore_options_from_backup_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToRestoreFromHardwareWalletPage() async { + await commonTestCases.tapItemByKey('restore_options_from_hardware_wallet_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future backAndVerify() async { + await commonTestCases.goBack(); + await isRestoreOptionsPage(); + } +} diff --git a/integration_test/robots/send_page_robot.dart b/integration_test/robots/send_page_robot.dart new file mode 100644 index 000000000..971556620 --- /dev/null +++ b/integration_test/robots/send_page_robot.dart @@ -0,0 +1,366 @@ +import 'dart:async'; + +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/send/send_page.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; +import '../components/common_test_constants.dart'; +import 'auth_page_robot.dart'; + +class SendPageRobot { + SendPageRobot({required this.tester}) + : commonTestCases = CommonTestCases(tester), + authPageRobot = AuthPageRobot(tester); + + WidgetTester tester; + CommonTestCases commonTestCases; + AuthPageRobot authPageRobot; + + Future isSendPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.send); + } + + void confirmViewComponentsDisplayProperly() { + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + commonTestCases.hasValueKey('send_page_address_textfield_key'); + commonTestCases.hasValueKey('send_page_note_textfield_key'); + commonTestCases.hasValueKey('send_page_amount_textfield_key'); + commonTestCases.hasValueKey('send_page_add_template_button_key'); + + if (sendViewModel.hasMultipleTokens) { + commonTestCases.hasValueKey('send_page_currency_picker_button_key'); + } + + if (!sendViewModel.isBatchSending) { + commonTestCases.hasValueKey('send_page_send_all_button_key'); + } + + if (!sendViewModel.isFiatDisabled) { + commonTestCases.hasValueKey('send_page_fiat_amount_textfield_key'); + } + + if (sendViewModel.hasFees) { + commonTestCases.hasValueKey('send_page_select_fee_priority_button_key'); + } + + if (sendViewModel.hasCoinControl) { + commonTestCases.hasValueKey('send_page_unspent_coin_button_key'); + } + + if (sendViewModel.hasCurrecyChanger) { + commonTestCases.hasValueKey('send_page_change_asset_button_key'); + } + + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) { + commonTestCases.hasValueKey('send_page_add_receiver_button_key'); + } + } + + Future selectReceiveCurrency(CryptoCurrency receiveCurrency) async { + final currencyPickerKey = 'send_page_currency_picker_button_key'; + final currencyPickerDialogKey = 'send_page_currency_picker_dialog_button_key'; + + await commonTestCases.tapItemByKey(currencyPickerKey); + commonTestCases.hasValueKey(currencyPickerDialogKey); + + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + if (receiveCurrency == sendViewModel.selectedCryptoCurrency) { + await commonTestCases + .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key'); + return; + } + + await commonTestCases.scrollUntilVisible( + 'picker_items_index_${receiveCurrency.name}_button_key', + 'picker_scrollbar_key', + ); + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key'); + } + + Future enterReceiveAddress(String receiveAddress) async { + await commonTestCases.enterText(receiveAddress, 'send_page_address_textfield_key'); + await commonTestCases.defaultSleepTime(); + } + + Future enterAmount(String amount) async { + await commonTestCases.enterText(amount, 'send_page_amount_textfield_key'); + } + + Future selectTransactionPriority({TransactionPriority? priority}) async { + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + if (!sendViewModel.hasFees || priority == null) return; + + final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key'; + await commonTestCases.tapItemByKey(transactionPriorityPickerKey); + + if (priority == sendViewModel.transactionPriority) { + await commonTestCases + .tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key'); + return; + } + + await commonTestCases.scrollUntilVisible( + 'picker_items_index_${priority.title}_button_key', + 'picker_scrollbar_key', + ); + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${priority.title}_button_key'); + } + + Future onSendButtonPressed() async { + tester.printToConsole('Pressing send'); + + await commonTestCases.tapItemByKey( + 'send_page_send_button_key', + shouldPumpAndSettle: false, + ); + + await _waitForSendTransactionCompletion(); + + await commonTestCases.defaultSleepTime(); + } + + Future _waitForSendTransactionCompletion() async { + await tester.pump(); + final Completer completer = Completer(); + + // Loop to wait for the async operation to complete + while (true) { + await Future.delayed(Duration(seconds: 1)); + + tester.printToConsole('Before _handleAuth'); + + await _handleAuthPage(); + + tester.printToConsole('After _handleAuth'); + + await tester.pump(); + + final sendPage = tester.widget(find.byType(SendPage)); + final state = sendPage.sendViewModel.state; + + await tester.pump(); + + bool isDone = state is ExecutedSuccessfullyState; + bool isFailed = state is FailureState; + + tester.printToConsole('isDone: $isDone'); + tester.printToConsole('isFailed: $isFailed'); + + if (isDone || isFailed) { + tester.printToConsole( + isDone ? 'Completer is done' : 'Completer is done though operation failed', + ); + completer.complete(); + await tester.pump(); + break; + } else { + tester.printToConsole('Completer is not done'); + await tester.pump(); + } + } + + await expectLater(completer.future, completes); + + tester.printToConsole('Done confirming sending operation'); + } + + Future _handleAuthPage() async { + tester.printToConsole('Inside _handleAuth'); + await tester.pump(); + tester.printToConsole('starting auth checks'); + + final authPage = authPageRobot.onAuthPage(); + + tester.printToConsole('hasAuth:$authPage'); + + if (authPage) { + await tester.pump(); + tester.printToConsole('Starting inner _handleAuth loop checks'); + + try { + await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + tester.printToConsole('Auth done'); + + await tester.pump(); + + tester.printToConsole('Auth pump done'); + } catch (e) { + tester.printToConsole('Auth failed, retrying'); + await tester.pump(); + _handleAuthPage(); + } + } + } + + Future handleSendResult() async { + tester.printToConsole('Inside handle function'); + + bool hasError = false; + + hasError = await hasErrorWhileSending(); + + tester.printToConsole('Has an Error in the handle: $hasError'); + + int maxRetries = 20; + int retries = 0; + + while (hasError && retries < maxRetries) { + tester.printToConsole('hasErrorInLoop: $hasError'); + await tester.pump(); + + await onSendFailureDialogButtonPressed(); + tester.printToConsole('Failure button tapped'); + + await commonTestCases.defaultSleepTime(); + + await onSendButtonPressed(); + tester.printToConsole('Send button tapped'); + + hasError = await hasErrorWhileSending(); + + retries++; + } + + if (!hasError) { + tester.printToConsole('No error, proceeding with flow'); + await tester.pump(); + } + + await commonTestCases.defaultSleepTime(); + } + + //* ------ On Sending Failure ------------ + Future hasErrorWhileSending() async { + await tester.pump(); + + tester.printToConsole('Checking if there is an error'); + + final errorDialog = find.byKey(ValueKey('send_page_send_failure_dialog_button_key')); + + bool hasError = errorDialog.tryEvaluate(); + + tester.printToConsole('Has error: $hasError'); + + return hasError; + } + + Future onSendFailureDialogButtonPressed() async { + await commonTestCases.defaultSleepTime(); + + tester.printToConsole('Send Button Failure Dialog Triggered'); + + await commonTestCases.tapItemByKey('send_page_send_failure_dialog_button_key'); + } + + //* ------ On Sending Success ------------ + Future onSendButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Inside confirm sending dialog: For sending'); + await commonTestCases.defaultSleepTime(); + await tester.pump(); + + final sendText = find.text(S.current.send).last; + bool hasText = sendText.tryEvaluate(); + tester.printToConsole('Has Text: $hasText'); + + if (hasText) { + await commonTestCases.tapItemByFinder(sendText, shouldPumpAndSettle: false); + // Loop to wait for the operation to commit transaction + await _waitForCommitTransactionCompletion(); + + await commonTestCases.defaultSleepTime(seconds: 4); + } else { + await commonTestCases.defaultSleepTime(); + await tester.pump(); + onSendButtonOnConfirmSendingDialogPressed(); + } + } + + Future _waitForCommitTransactionCompletion() async { + final Completer completer = Completer(); + + while (true) { + await Future.delayed(Duration(seconds: 1)); + + final sendPage = tester.widget(find.byType(SendPage)); + final state = sendPage.sendViewModel.state; + + bool isDone = state is TransactionCommitted; + bool isFailed = state is FailureState; + + tester.printToConsole('isDone: $isDone'); + tester.printToConsole('isFailed: $isFailed'); + + if (isDone || isFailed) { + tester.printToConsole( + isDone ? 'Completer is done' : 'Completer is done though operation failed', + ); + completer.complete(); + await tester.pump(); + break; + } else { + tester.printToConsole('Completer is not done'); + await tester.pump(); + } + } + + await expectLater(completer.future, completes); + + tester.printToConsole('Done Committing Transaction'); + } + + Future onCancelButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Inside confirm sending dialog: For canceling'); + await commonTestCases.defaultSleepTime(seconds: 4); + + final cancelText = find.text(S.current.cancel); + bool hasText = cancelText.tryEvaluate(); + + if (hasText) { + await commonTestCases.tapItemByFinder(cancelText); + + await commonTestCases.defaultSleepTime(seconds: 4); + } + } + + //* ---- Add Contact Dialog On Send Successful Dialog ----- + Future onSentDialogPopUp() async { + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + final newContactAddress = sendPage.newContactAddress ?? sendViewModel.newContactAddress(); + if (newContactAddress != null) { + await _onAddContactButtonOnSentDialogPressed(); + } + + await commonTestCases.defaultSleepTime(); + } + + Future _onAddContactButtonOnSentDialogPressed() async { + await commonTestCases.tapItemByKey('send_page_sent_dialog_add_contact_button_key'); + } + + // ignore: unused_element + Future _onIgnoreButtonOnSentDialogPressed() async { + await commonTestCases.tapItemByKey('send_page_sent_dialog_ignore_button_key'); + } +} \ No newline at end of file diff --git a/integration_test/robots/setup_pin_code_robot.dart b/integration_test/robots/setup_pin_code_robot.dart new file mode 100644 index 000000000..0888aac30 --- /dev/null +++ b/integration_test/robots/setup_pin_code_robot.dart @@ -0,0 +1,28 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; +import 'pin_code_widget_robot.dart'; + +class SetupPinCodeRobot extends PinCodeWidgetRobot { + SetupPinCodeRobot(this.tester) + : commonTestCases = CommonTestCases(tester), + super(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isSetupPinCodePage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.setup_pin); + } + + Future tapSuccessButton() async { + await commonTestCases.tapItemByKey('setup_pin_code_success_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/welcome_page_robot.dart b/integration_test/robots/welcome_page_robot.dart new file mode 100644 index 000000000..510f63556 --- /dev/null +++ b/integration_test/robots/welcome_page_robot.dart @@ -0,0 +1,40 @@ +import 'package:cake_wallet/src/screens/welcome/welcome_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class WelcomePageRobot { + WelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isWelcomePage() async { + await commonTestCases.isSpecificPage(); + } + + void confirmActionButtonsDisplay() { + final createNewWalletButton = find.byKey(ValueKey('welcome_page_create_new_wallet_button_key')); + + final restoreWalletButton = find.byKey(ValueKey('welcome_page_restore_wallet_button_key')); + + expect(createNewWalletButton, findsOneWidget); + expect(restoreWalletButton, findsOneWidget); + } + + Future navigateToCreateNewWalletPage() async { + await commonTestCases.tapItemByKey('welcome_page_create_new_wallet_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToRestoreWalletPage() async { + await commonTestCases.tapItemByKey('welcome_page_restore_wallet_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future backAndVerify() async { + await commonTestCases.goBack(); + await isWelcomePage(); + } +} diff --git a/integration_test/test_suites/exchange_flow_test.dart b/integration_test/test_suites/exchange_flow_test.dart new file mode 100644 index 000000000..6c993634c --- /dev/null +++ b/integration_test/test_suites/exchange_flow_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/auth_page_robot.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/exchange_confirm_page_robot.dart'; +import '../robots/exchange_page_robot.dart'; +import '../robots/exchange_trade_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + AuthPageRobot authPageRobot; + CommonTestFlows commonTestFlows; + ExchangePageRobot exchangePageRobot; + DashboardPageRobot dashboardPageRobot; + ExchangeTradePageRobot exchangeTradePageRobot; + ExchangeConfirmPageRobot exchangeConfirmPageRobot; + + group('Exchange Flow Tests', () { + testWidgets('Exchange flow', (tester) async { + authPageRobot = AuthPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + exchangePageRobot = ExchangePageRobot(tester); + dashboardPageRobot = DashboardPageRobot(tester); + exchangeTradePageRobot = ExchangeTradePageRobot(tester); + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); + + await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key')); + await commonTestFlows.restoreWalletThroughSeedsFlow(); + await dashboardPageRobot.navigateToExchangePage(); + + // ----------- Exchange Page ------------- + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.enterDepositRefundAddress( + depositAddress: CommonTestConstants.testWalletAddress, + ); + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + + await exchangePageRobot.onExchangeButtonPressed(); + + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); + + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + } + + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); + await exchangeTradePageRobot.onGotItButtonPressed(); + }); + }); +} diff --git a/integration_test/test_suites/send_flow_test.dart b/integration_test/test_suites/send_flow_test.dart new file mode 100644 index 000000000..38ac1574f --- /dev/null +++ b/integration_test/test_suites/send_flow_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/send_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + SendPageRobot sendPageRobot; + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + + group('Send Flow Tests', () { + testWidgets('Send flow', (tester) async { + commonTestFlows = CommonTestFlows(tester); + sendPageRobot = SendPageRobot(tester: tester); + dashboardPageRobot = DashboardPageRobot(tester); + + await commonTestFlows.startAppFlow(ValueKey('send_test_app_key')); + await commonTestFlows.restoreWalletThroughSeedsFlow(); + await dashboardPageRobot.navigateToSendPage(); + + await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + await sendPageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + await sendPageRobot.enterAmount(CommonTestConstants.sendTestAmount); + await sendPageRobot.selectTransactionPriority(); + + await sendPageRobot.onSendButtonPressed(); + + await sendPageRobot.handleSendResult(); + + await sendPageRobot.onSendButtonOnConfirmSendingDialogPressed(); + + await sendPageRobot.onSentDialogPopUp(); + }); + }); +} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 39dcd8b80..847769cba 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -66,6 +66,8 @@ PODS: - Toast - in_app_review (0.2.0): - Flutter + - integration_test (0.0.1): + - Flutter - MTBBarcodeScanner (5.0.11) - OrderedSet (5.0.0) - package_info_plus (0.4.5): @@ -120,6 +122,8 @@ DEPENDENCIES: - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - package_info (from `.symlinks/plugins/package_info/ios`) - 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`) @@ -174,6 +178,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluttertoast/ios" in_app_review: :path: ".symlinks/plugins/in_app_review/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + package_info: + :path: ".symlinks/plugins/package_info/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -216,6 +224,7 @@ SPEC CHECKSUMS: flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d + integration_test: 13825b8a9334a850581300559b8839134b124670 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c diff --git a/lib/main.dart b/lib/main.dart index cae528210..32a6397c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,11 +47,11 @@ final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); final RouteObserver> routeObserver = RouteObserver>(); -Future main() async { - await runAppWithZone(); +Future main({Key? topLevelKey}) async { + await runAppWithZone(topLevelKey: topLevelKey); } -Future runAppWithZone() async { +Future runAppWithZone({Key? topLevelKey}) async { bool isAppRunning = false; await runZonedGuarded(() async { @@ -67,7 +67,8 @@ Future runAppWithZone() async { }; await initializeAppAtRoot(); - runApp(App()); + runApp(App(key: topLevelKey)); + isAppRunning = true; }, (error, stackTrace) async { if (!isAppRunning) { @@ -236,6 +237,9 @@ Future initialSetup( } class App extends StatefulWidget { + App({this.key}); + + final Key? key; @override AppState createState() => AppState(); } @@ -264,7 +268,7 @@ class AppState extends State with SingleTickerProviderStateMixin { statusBarIconBrightness: statusBarIconBrightness)); return Root( - key: rootKey, + key: widget.key ?? rootKey, appStore: appStore, authenticationStore: authenticationStore, navigatorKey: navigatorKey, diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index ad6e68cd8..953463269 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -147,6 +147,7 @@ class _DashboardPageView extends BasePage { return Observer( builder: (context) { return ServicesUpdatesWidget( + key: ValueKey('dashboard_page_services_update_button_key'), dashboardViewModel.getServicesStatus(), enabled: dashboardViewModel.isEnabledBulletinAction, ); @@ -157,6 +158,7 @@ class _DashboardPageView extends BasePage { @override Widget middle(BuildContext context) { return SyncIndicator( + key: ValueKey('dashboard_page_sync_indicator_button_key'), dashboardViewModel: dashboardViewModel, onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync), ); @@ -173,6 +175,7 @@ class _DashboardPageView extends BasePage { alignment: Alignment.centerRight, width: 40, child: TextButton( + key: ValueKey('dashboard_page_wallet_menu_button_key'), // FIX-ME: Style //highlightColor: Colors.transparent, //splashColor: Colors.transparent, @@ -226,6 +229,7 @@ class _DashboardPageView extends BasePage { child: Observer( builder: (context) { return PageView.builder( + key: ValueKey('dashboard_page_view_key'), controller: controller, itemCount: pages.length, itemBuilder: (context, index) => pages[index], @@ -291,6 +295,8 @@ class _DashboardPageView extends BasePage { button: true, enabled: (action.isEnabled?.call(dashboardViewModel) ?? true), child: ActionButton( + key: ValueKey( + 'dashboard_page_${action.name(context)}_action_button_key'), image: Image.asset( action.image, height: 24, diff --git a/lib/src/screens/dashboard/widgets/action_button.dart b/lib/src/screens/dashboard/widgets/action_button.dart index 23f5c2f93..49ebab3cd 100644 --- a/lib/src/screens/dashboard/widgets/action_button.dart +++ b/lib/src/screens/dashboard/widgets/action_button.dart @@ -2,13 +2,15 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; class ActionButton extends StatelessWidget { - ActionButton( - {required this.image, - required this.title, - this.route, - this.onClick, - this.alignment = Alignment.center, - this.textColor}); + ActionButton({ + required this.image, + required this.title, + this.route, + this.onClick, + this.alignment = Alignment.center, + this.textColor, + super.key, + }); final Image image; final String title; diff --git a/lib/src/screens/dashboard/widgets/sync_indicator.dart b/lib/src/screens/dashboard/widgets/sync_indicator.dart index 52e596a83..27b3d0109 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator.dart @@ -7,7 +7,11 @@ import 'package:cw_core/sync_status.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; class SyncIndicator extends StatelessWidget { - SyncIndicator({required this.dashboardViewModel, required this.onTap}); + SyncIndicator({ + required this.dashboardViewModel, + required this.onTap, + super.key, + }); final DashboardViewModel dashboardViewModel; final Function() onTap; diff --git a/lib/src/screens/disclaimer/disclaimer_page.dart b/lib/src/screens/disclaimer/disclaimer_page.dart index f82a9efbe..c9d959b40 100644 --- a/lib/src/screens/disclaimer/disclaimer_page.dart +++ b/lib/src/screens/disclaimer/disclaimer_page.dart @@ -207,6 +207,7 @@ class DisclaimerBodyState extends State { padding: EdgeInsets.only( left: 24.0, top: 10.0, right: 24.0, bottom: 10.0), child: InkWell( + key: ValueKey('disclaimer_check_key'), onTap: () { setState(() { _checked = !_checked; @@ -230,6 +231,7 @@ class DisclaimerBodyState extends State { color: Theme.of(context).colorScheme.background), child: _checked ? Icon( + key: ValueKey('disclaimer_check_icon_key'), Icons.check, color: Colors.blue, size: 20.0, @@ -253,6 +255,7 @@ class DisclaimerBodyState extends State { padding: EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0), child: PrimaryButton( + key: ValueKey('disclaimer_accept_button_key'), onPressed: _checked ? () => Navigator.of(context) .popAndPushNamed(Routes.welcome) diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 2c717a3c8..78b4d0db8 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -228,6 +228,7 @@ class ExchangePage extends BasePage { ), Observer( builder: (_) => LoadingPrimaryButton( + key: ValueKey('exchange_page_exchange_button_key'), text: S.of(context).exchange, onPressed: () { if (_formKey.currentState != null && @@ -430,6 +431,8 @@ class ExchangePage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( + key: ValueKey('exchange_page_trade_creation_failure_dialog_key'), + buttonKey: ValueKey('exchange_page_trade_creation_failure_dialog_button_key'), alertTitle: S.of(context).provider_error(state.title), alertContent: state.error, buttonText: S.of(context).ok, @@ -612,6 +615,7 @@ class ExchangePage extends BasePage { Widget _exchangeCardsSection(BuildContext context) { final firstExchangeCard = Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'deposit_exchange_card', onDispose: disposeBestRateSync, hasAllAmount: exchangeViewModel.hasAllAmount, allAmount: exchangeViewModel.hasAllAmount @@ -681,6 +685,7 @@ class ExchangePage extends BasePage { final secondExchangeCard = Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'receive_exchange_card', onDispose: disposeBestRateSync, amountFocusNode: _receiveAmountFocus, addressFocusNode: _receiveAddressFocus, diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index d24c91dad..4edc9095a 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -121,6 +121,7 @@ class ExchangeTemplatePage extends BasePage { padding: EdgeInsets.fromLTRB(24, 100, 24, 32), child: Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'deposit_exchange_template_card', amountFocusNode: _depositAmountFocus, key: depositKey, title: S.of(context).you_will_send, @@ -157,6 +158,7 @@ class ExchangeTemplatePage extends BasePage { padding: EdgeInsets.only(top: 29, left: 24, right: 24), child: Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'receive_exchange_template_card', amountFocusNode: _receiveAmountFocus, key: receiveKey, title: S.of(context).you_will_get, diff --git a/lib/src/screens/exchange/widgets/currency_picker.dart b/lib/src/screens/exchange/widgets/currency_picker.dart index 0fe1d4e67..8c6f5e214 100644 --- a/lib/src/screens/exchange/widgets/currency_picker.dart +++ b/lib/src/screens/exchange/widgets/currency_picker.dart @@ -12,7 +12,8 @@ class CurrencyPicker extends StatefulWidget { this.title, this.hintText, this.isMoneroWallet = false, - this.isConvertFrom = false}); + this.isConvertFrom = false, + super.key}); final int selectedAtIndex; final List items; diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 02218f848..75a2eadd7 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -19,34 +19,35 @@ import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; class ExchangeCard extends StatefulWidget { - ExchangeCard( - {Key? key, - required this.initialCurrency, - required this.initialAddress, - required this.initialWalletName, - required this.initialIsAmountEditable, - required this.isAmountEstimated, - required this.currencies, - required this.onCurrencySelected, - this.imageArrow, - this.currencyValueValidator, - this.addressTextFieldValidator, - this.title = '', - this.initialIsAddressEditable = true, - this.hasRefundAddress = false, - this.isMoneroWallet = false, - this.currencyButtonColor = Colors.transparent, - this.addressButtonsColor = Colors.transparent, - this.borderColor = Colors.transparent, - this.hasAllAmount = false, - this.isAllAmountEnabled = false, - this.amountFocusNode, - this.addressFocusNode, - this.allAmount, - this.onPushPasteButton, - this.onPushAddressBookButton, - this.onDispose}) - : super(key: key); + ExchangeCard({ + Key? key, + required this.initialCurrency, + required this.initialAddress, + required this.initialWalletName, + required this.initialIsAmountEditable, + required this.isAmountEstimated, + required this.currencies, + required this.onCurrencySelected, + this.imageArrow, + this.currencyValueValidator, + this.addressTextFieldValidator, + this.title = '', + this.initialIsAddressEditable = true, + this.hasRefundAddress = false, + this.isMoneroWallet = false, + this.currencyButtonColor = Colors.transparent, + this.addressButtonsColor = Colors.transparent, + this.borderColor = Colors.transparent, + this.hasAllAmount = false, + this.isAllAmountEnabled = false, + this.amountFocusNode, + this.addressFocusNode, + this.allAmount, + this.onPushPasteButton, + this.onPushAddressBookButton, + this.onDispose, + required this.cardInstanceName, + }) : super(key: key); final List currencies; final Function(CryptoCurrency) onCurrencySelected; @@ -74,6 +75,7 @@ class ExchangeCard extends StatefulWidget { final void Function(BuildContext context)? onPushPasteButton; final void Function(BuildContext context)? onPushAddressBookButton; final Function()? onDispose; + final String cardInstanceName; @override ExchangeCardState createState() => ExchangeCardState(); @@ -89,11 +91,13 @@ class ExchangeCardState extends State { _walletName = '', _selectedCurrency = CryptoCurrency.btc, _isAmountEstimated = false, - _isMoneroWallet = false; + _isMoneroWallet = false, + _cardInstanceName = ''; final addressController = TextEditingController(); final amountController = TextEditingController(); + String _cardInstanceName; String _title; String? _min; String? _max; @@ -106,6 +110,7 @@ class ExchangeCardState extends State { @override void initState() { + _cardInstanceName = widget.cardInstanceName; _title = widget.title; _isAmountEditable = widget.initialIsAmountEditable; _isAddressEditable = widget.initialIsAddressEditable; @@ -184,6 +189,7 @@ class ExchangeCardState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ Text( + key: ValueKey('${_cardInstanceName}_title_key'), _title, style: TextStyle( fontSize: 18, @@ -193,17 +199,26 @@ class ExchangeCardState extends State { ], ), CurrencyAmountTextField( - imageArrow: widget.imageArrow, - selectedCurrency: _selectedCurrency.toString(), - amountFocusNode: widget.amountFocusNode, - amountController: amountController, - onTapPicker: () => _presentPicker(context), - isAmountEditable: _isAmountEditable, - isPickerEnable: true, - allAmountButton: widget.hasAllAmount, - currencyValueValidator: widget.currencyValueValidator, - tag: _selectedCurrency.tag, - allAmountCallback: widget.allAmount), + currencyPickerButtonKey: ValueKey('${_cardInstanceName}_currency_picker_button_key'), + selectedCurrencyTextKey: ValueKey('${_cardInstanceName}_selected_currency_text_key'), + selectedCurrencyTagTextKey: + ValueKey('${_cardInstanceName}_selected_currency_tag_text_key'), + amountTextfieldKey: ValueKey('${_cardInstanceName}_amount_textfield_key'), + sendAllButtonKey: ValueKey('${_cardInstanceName}_send_all_button_key'), + currencyAmountTextFieldWidgetKey: + ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'), + imageArrow: widget.imageArrow, + selectedCurrency: _selectedCurrency.toString(), + amountFocusNode: widget.amountFocusNode, + amountController: amountController, + onTapPicker: () => _presentPicker(context), + isAmountEditable: _isAmountEditable, + isPickerEnable: true, + allAmountButton: widget.hasAllAmount, + currencyValueValidator: widget.currencyValueValidator, + tag: _selectedCurrency.tag, + allAmountCallback: widget.allAmount, + ), Divider(height: 1, color: Theme.of(context).extension()!.textFieldHintColor), Padding( padding: EdgeInsets.only(top: 5), @@ -212,6 +227,7 @@ class ExchangeCardState extends State { child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ _min != null ? Text( + key: ValueKey('${_cardInstanceName}_min_limit_text_key'), S.of(context).min_value(_min ?? '', _selectedCurrency.toString()), style: TextStyle( fontSize: 10, @@ -221,11 +237,15 @@ class ExchangeCardState extends State { : Offstage(), _min != null ? SizedBox(width: 10) : Offstage(), _max != null - ? Text(S.of(context).max_value(_max ?? '', _selectedCurrency.toString()), + ? Text( + key: ValueKey('${_cardInstanceName}_max_limit_text_key'), + S.of(context).max_value(_max ?? '', _selectedCurrency.toString()), style: TextStyle( - fontSize: 10, - height: 1.2, - color: Theme.of(context).extension()!.hintTextColor)) + fontSize: 10, + height: 1.2, + color: Theme.of(context).extension()!.hintTextColor, + ), + ) : Offstage(), ])), ), @@ -246,6 +266,7 @@ class ExchangeCardState extends State { child: Padding( padding: EdgeInsets.only(top: 20), child: AddressTextField( + addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'), focusNode: widget.addressFocusNode, controller: addressController, onURIScanned: (uri) { @@ -286,6 +307,8 @@ class ExchangeCardState extends State { FocusTraversalOrder( order: NumericFocusOrder(3), child: BaseTextFormField( + key: ValueKey( + '${_cardInstanceName}_non_editable_address_textfield_key'), controller: addressController, borderColor: Colors.transparent, suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36), @@ -309,6 +332,8 @@ class ExchangeCardState extends State { child: Semantics( label: S.of(context).address_book, child: InkWell( + key: ValueKey( + '${_cardInstanceName}_address_book_button_key'), onTap: () async { final contact = await Navigator.of(context).pushNamed( @@ -346,6 +371,8 @@ class ExchangeCardState extends State { child: Semantics( label: S.of(context).copy_address, child: InkWell( + key: ValueKey( + '${_cardInstanceName}_copy_refund_address_button_key'), onTap: () { Clipboard.setData( ClipboardData(text: addressController.text)); @@ -369,6 +396,7 @@ class ExchangeCardState extends State { showPopUp( context: context, builder: (_) => CurrencyPicker( + key: ValueKey('${_cardInstanceName}_currency_picker_dialog_button_key'), selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), items: widget.currencies, hintText: S.of(context).search_currency, diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index bf307dce6..a179ee473 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -83,6 +83,7 @@ class ExchangeConfirmPage extends BasePage { padding: EdgeInsets.fromLTRB(10, 0, 10, 10), child: Builder( builder: (context) => PrimaryButton( + key: ValueKey('exchange_confirm_page_copy_to_clipboard_button_key'), onPressed: () { Clipboard.setData(ClipboardData(text: trade.id)); showBar( @@ -117,6 +118,7 @@ class ExchangeConfirmPage extends BasePage { ], )), PrimaryButton( + key: ValueKey('exchange_confirm_page_saved_id_button_key'), onPressed: () => Navigator.of(context) .pushReplacementNamed(Routes.exchangeTrade), text: S.of(context).saved_the_trade_id, diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 0766a4562..0f3cc7bd9 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -39,7 +39,9 @@ void showInformation( showPopUp( context: context, - builder: (_) => InformationPage(information: information)); + builder: (_) => InformationPage( + key: ValueKey('information_page_dialog_key'), + information: information)); } class ExchangeTradePage extends BasePage { @@ -215,6 +217,7 @@ class ExchangeTradeState extends State { return widget.exchangeTradeViewModel.isSendable && !(sendingState is TransactionCommitted) ? LoadingPrimaryButton( + key: ValueKey('exchange_trade_page_confirm_sending_button_key'), isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty, isLoading: sendingState is IsExecutingState, @@ -241,6 +244,8 @@ class ExchangeTradeState extends State { context: context, builder: (BuildContext popupContext) { return AlertWithOneAction( + key: ValueKey('exchange_trade_page_send_failure_dialog_key'), + buttonKey: ValueKey('exchange_trade_page_send_failure_dialog_button_key'), alertTitle: S.of(popupContext).error, alertContent: state.error, buttonText: S.of(popupContext).ok, @@ -255,6 +260,10 @@ class ExchangeTradeState extends State { context: context, builder: (BuildContext popupContext) { return ConfirmSendingAlert( + key: ValueKey('exchange_trade_page_confirm_sending_dialog_key'), + alertLeftActionButtonKey: ValueKey('exchange_trade_page_confirm_sending_dialog_cancel_button_key'), + alertRightActionButtonKey: + ValueKey('exchange_trade_page_confirm_sending_dialog_send_button_key'), alertTitle: S.of(popupContext).confirm_sending, amount: S.of(popupContext).send_amount, amountValue: widget.exchangeTradeViewModel.sendViewModel diff --git a/lib/src/screens/exchange_trade/information_page.dart b/lib/src/screens/exchange_trade/information_page.dart index eed124b97..114a4824d 100644 --- a/lib/src/screens/exchange_trade/information_page.dart +++ b/lib/src/screens/exchange_trade/information_page.dart @@ -10,7 +10,7 @@ import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; class InformationPage extends StatelessWidget { - InformationPage({required this.information}); + InformationPage({required this.information, super.key}); final String information; @@ -47,6 +47,7 @@ class InformationPage extends StatelessWidget { Padding( padding: EdgeInsets.fromLTRB(10, 0, 10, 10), child: PrimaryButton( + key: ValueKey('information_page_got_it_button_key'), onPressed: () => Navigator.of(context).pop(), text: S.of(context).got_it, color: Theme.of(context).extension()!.buttonBackgroundColor, diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index faef4b479..6cf21ae58 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -131,6 +131,7 @@ class WalletTypeFormState extends State { Expanded( child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + scrollableKey: ValueKey('new_wallet_type_scrollable_key'), content: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -138,6 +139,7 @@ class WalletTypeFormState extends State { (type) => Padding( padding: EdgeInsets.only(top: 12), child: SelectButton( + key: ValueKey('new_wallet_type_${type.name}_button_key'), image: Image.asset( walletTypeToCryptoCurrency(type).iconPath ?? '', height: 24, @@ -158,6 +160,7 @@ class WalletTypeFormState extends State { ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: PrimaryButton( + key: ValueKey('new_wallet_type_next_button_key'), onPressed: () => onTypeSelected(), text: S.of(context).seed_language_next, color: Theme.of(context).primaryColor, diff --git a/lib/src/screens/new_wallet/widgets/select_button.dart b/lib/src/screens/new_wallet/widgets/select_button.dart index 834d85986..87015b89e 100644 --- a/lib/src/screens/new_wallet/widgets/select_button.dart +++ b/lib/src/screens/new_wallet/widgets/select_button.dart @@ -20,6 +20,7 @@ class SelectButton extends StatelessWidget { this.deviceConnectionTypes, this.borderRadius, this.padding, + super.key, }); final Widget? image; diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index 36328aee2..d39c88cc9 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -240,6 +240,7 @@ class PinCodeState extends State { return Container( margin: EdgeInsets.only(left: marginLeft, right: marginRight), child: TextButton( + key: ValueKey('pin_code_button_${index}_key'), onPressed: () => _push(index), style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.background, diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart index ce3de9a6c..382c53f9d 100644 --- a/lib/src/screens/receive/widgets/currency_input_field.dart +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -24,8 +24,20 @@ class CurrencyAmountTextField extends StatelessWidget { this.tagBackgroundColor, this.currencyValueValidator, this.allAmountCallback, - }); + this.sendAllButtonKey, + this.amountTextfieldKey, + this.currencyPickerButtonKey, + this.selectedCurrencyTextKey, + this.selectedCurrencyTagTextKey, + this.currencyAmountTextFieldWidgetKey, + }) : super(key: currencyAmountTextFieldWidgetKey); + final Key? sendAllButtonKey; + final Key? amountTextfieldKey; + final Key? currencyPickerButtonKey; + final Key? selectedCurrencyTextKey; + final Key? selectedCurrencyTagTextKey; + final Key? currencyAmountTextFieldWidgetKey; final Widget? imageArrow; final String selectedCurrency; final String? tag; @@ -54,6 +66,7 @@ class CurrencyAmountTextField extends StatelessWidget { ? Container( height: 32, child: InkWell( + key: currencyPickerButtonKey, onTap: onTapPicker, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -65,6 +78,7 @@ class CurrencyAmountTextField extends StatelessWidget { Image.asset('assets/images/arrow_bottom_purple_icon.png', color: textColor, height: 8)), Text( + key: selectedCurrencyTextKey, selectedCurrency, style: TextStyle( fontWeight: FontWeight.w600, @@ -77,6 +91,7 @@ class CurrencyAmountTextField extends StatelessWidget { ), ) : Text( + key: selectedCurrencyTextKey, selectedCurrency, style: TextStyle( fontWeight: FontWeight.w600, @@ -98,6 +113,7 @@ class CurrencyAmountTextField extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(6.0), child: Text( + key: selectedCurrencyTagTextKey, tag!, style: TextStyle( fontSize: 12, @@ -132,9 +148,9 @@ class CurrencyAmountTextField extends StatelessWidget { padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), margin: const EdgeInsets.only(right: 3), decoration: BoxDecoration( - border: Border.all( - color: textColor, - ), + border: Border.all( + color: textColor, + ), borderRadius: BorderRadius.circular(26), color: Theme.of(context).primaryColor)) : _prefixContent, @@ -146,6 +162,7 @@ class CurrencyAmountTextField extends StatelessWidget { child: FocusTraversalOrder( order: NumericFocusOrder(1), child: BaseTextFormField( + key: amountTextfieldKey, focusNode: amountFocusNode, controller: amountController, enabled: isAmountEditable, @@ -184,6 +201,7 @@ class CurrencyAmountTextField extends StatelessWidget { borderRadius: const BorderRadius.all(Radius.circular(6)), ), child: InkWell( + key: sendAllButtonKey, onTap: allAmountCallback, child: Center( child: Text( diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index a703c9f9e..cb5086fe1 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -59,8 +59,12 @@ class RestoreOptionsPage extends BasePage { child: Column( children: [ OptionTile( - onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromSeedKeys, - arguments: isNewInstall), + key: ValueKey('restore_options_from_seeds_button_key'), + onPressed: () => Navigator.pushNamed( + context, + Routes.restoreWalletFromSeedKeys, + arguments: isNewInstall, + ), image: imageSeedKeys, title: S.of(context).restore_title_from_seed_keys, description: S.of(context).restore_description_from_seed_keys, @@ -69,6 +73,7 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( + key: ValueKey('restore_options_from_backup_button_key'), onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), image: imageBackup, title: S.of(context).restore_title_from_backup, @@ -79,6 +84,7 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( + key: ValueKey('restore_options_from_hardware_wallet_button_key'), onPressed: () => Navigator.pushNamed( context, Routes.restoreWalletFromHardwareWallet, arguments: isNewInstall), @@ -90,10 +96,12 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - onPressed: () => _onScanQRCode(context), - image: qrCode, - title: S.of(context).scan_qr_code, - description: S.of(context).cold_or_recover_wallet), + key: ValueKey('restore_options_from_qr_button_key'), + onPressed: () => _onScanQRCode(context), + image: qrCode, + title: S.of(context).scan_qr_code, + description: S.of(context).cold_or_recover_wallet, + ), ) ], ), diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 56e49b087..83772f866 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -112,10 +112,12 @@ class WalletRestoreFromKeysFromState extends State { alignment: Alignment.centerRight, children: [ BaseTextFormField( + key: ValueKey('wallet_restore_from_keys_wallet_name_textfield_key'), controller: nameTextEditingController, hintText: S.of(context).wallet_name, validator: WalletNameValidator(), suffixIcon: IconButton( + key: ValueKey('wallet_restore_from_keys_wallet_name_refresh_button_key'), onPressed: () async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); @@ -175,6 +177,7 @@ class WalletRestoreFromKeysFromState extends State { bool nanoBased = widget.walletRestoreViewModel.type == WalletType.nano || widget.walletRestoreViewModel.type == WalletType.banano; return AddressTextField( + addressKey: ValueKey('wallet_restore_from_key_private_key_textfield_key'), controller: privateKeyController, placeholder: nanoBased ? S.of(context).seed_hex_form : S.of(context).private_key, options: [AddressTextFieldOption.paste], 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 f295aab13..67576144c 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -151,11 +151,13 @@ class WalletRestoreFromSeedFormState extends State { alignment: Alignment.centerRight, children: [ BaseTextFormField( + key: ValueKey('wallet_restore_from_seed_wallet_name_textfield_key'), controller: nameTextEditingController, hintText: S .of(context) .wallet_name, suffixIcon: IconButton( + key: ValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key'), onPressed: () async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); @@ -190,10 +192,13 @@ class WalletRestoreFromSeedFormState extends State { )), Container(height: 20), SeedWidget( - key: seedWidgetStateKey, - language: language, - type: widget.type, - onSeedChange: onSeedChange), + key: seedWidgetStateKey, + language: language, + type: widget.type, + onSeedChange: onSeedChange, + seedTextFieldKey: ValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key'), + pasteButtonKey: ValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'), + ), if (widget.type == WalletType.monero || widget.type == WalletType.wownero) GestureDetector( onTap: () async { diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index a855088e4..c8e109860 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -213,6 +213,7 @@ class WalletRestorePage extends BasePage { Observer( builder: (context) { return LoadingPrimaryButton( + key: ValueKey('wallet_restore_seed_or_key_restore_button_key'), onPressed: () async { await _confirmForm(context); }, @@ -230,6 +231,7 @@ class WalletRestorePage extends BasePage { ), const SizedBox(height: 25), GestureDetector( + key: ValueKey('wallet_restore_advanced_settings_button_key'), onTap: () { Navigator.of(context) .pushNamed(Routes.advancedPrivacySettings, arguments: { diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 3e906048d..ccf4a1dc4 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -250,6 +250,7 @@ class SendPage extends BasePage { return Row( children: [ AddTemplateButton( + key: ValueKey('send_page_add_template_button_key'), onTap: () => Navigator.of(context).pushNamed(Routes.sendTemplate), currentTemplatesLength: templates.length, ), @@ -339,19 +340,22 @@ class SendPage extends BasePage { children: [ if (sendViewModel.hasCurrecyChanger) Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - onPressed: () => presentCurrencyPicker(context), - text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', - color: Colors.transparent, - textColor: - Theme.of(context).extension()!.hintTextColor, - ))), + builder: (_) => Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + key: ValueKey('send_page_change_asset_button_key'), + onPressed: () => presentCurrencyPicker(context), + text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', + color: Colors.transparent, + textColor: Theme.of(context).extension()!.hintTextColor, + ), + ), + ), if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) Padding( padding: EdgeInsets.only(bottom: 12), child: PrimaryButton( + key: ValueKey('send_page_add_receiver_button_key'), onPressed: () { sendViewModel.addOutput(); Future.delayed(const Duration(milliseconds: 250), () { @@ -368,6 +372,7 @@ class SendPage extends BasePage { Observer( builder: (_) { return LoadingPrimaryButton( + key: ValueKey('send_page_send_button_key'), onPressed: () async { if (sendViewModel.state is IsExecutingState) return; if (_formKey.currentState != null && !_formKey.currentState!.validate()) { @@ -451,6 +456,8 @@ class SendPage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( + key: ValueKey('send_page_send_failure_dialog_key'), + buttonKey: ValueKey('send_page_send_failure_dialog_button_key'), alertTitle: S.of(context).error, alertContent: state.error, buttonText: S.of(context).ok, @@ -466,6 +473,7 @@ class SendPage extends BasePage { context: context, builder: (BuildContext _dialogContext) { return ConfirmSendingAlert( + key: ValueKey('send_page_confirm_sending_dialog_key'), alertTitle: S.of(_dialogContext).confirm_sending, amount: S.of(_dialogContext).send_amount, amountValue: sendViewModel.pendingTransaction!.amountFormatted, @@ -480,6 +488,10 @@ class SendPage extends BasePage { change: sendViewModel.pendingTransaction!.change, rightButtonText: S.of(_dialogContext).send, leftButtonText: S.of(_dialogContext).cancel, + alertRightActionButtonKey: + ValueKey('send_page_confirm_sending_dialog_send_button_key'), + alertLeftActionButtonKey: + ValueKey('send_page_confirm_sending_dialog_cancel_button_key'), actionRightButton: () async { Navigator.of(_dialogContext).pop(); sendViewModel.commitTransaction(); @@ -513,10 +525,15 @@ class SendPage extends BasePage { if (newContactAddress != null) { return AlertWithTwoActions( + alertDialogKey: ValueKey('send_page_sent_dialog_key'), alertTitle: '', alertContent: alertContent, rightButtonText: S.of(_dialogContext).add_contact, leftButtonText: S.of(_dialogContext).ignor, + alertLeftActionButtonKey: + ValueKey('send_page_sent_dialog_ignore_button_key'), + alertRightActionButtonKey: ValueKey( + 'send_page_sent_dialog_add_contact_button_key'), actionRightButton: () { Navigator.of(_dialogContext).pop(); RequestReviewHandler.requestReview(); diff --git a/lib/src/screens/send/widgets/confirm_sending_alert.dart b/lib/src/screens/send/widgets/confirm_sending_alert.dart index c7b6d3407..83866a73c 100644 --- a/lib/src/screens/send/widgets/confirm_sending_alert.dart +++ b/lib/src/screens/send/widgets/confirm_sending_alert.dart @@ -9,30 +9,34 @@ import 'package:cake_wallet/src/widgets/cake_scrollbar.dart'; import 'package:flutter/scheduler.dart'; class ConfirmSendingAlert extends BaseAlertDialog { - ConfirmSendingAlert( - {required this.alertTitle, - this.paymentId, - this.paymentIdValue, - this.expirationTime, - required this.amount, - required this.amountValue, - required this.fiatAmountValue, - required this.fee, - this.feeRate, - required this.feeValue, - required this.feeFiatAmount, - required this.outputs, - this.change, - required this.leftButtonText, - required this.rightButtonText, - required this.actionLeftButton, - required this.actionRightButton, - this.alertBarrierDismissible = true, - this.alertLeftActionButtonTextColor, - this.alertRightActionButtonTextColor, - this.alertLeftActionButtonColor, - this.alertRightActionButtonColor, - this.onDispose}); + ConfirmSendingAlert({ + required this.alertTitle, + this.paymentId, + this.paymentIdValue, + this.expirationTime, + required this.amount, + required this.amountValue, + required this.fiatAmountValue, + required this.fee, + this.feeRate, + required this.feeValue, + required this.feeFiatAmount, + required this.outputs, + this.change, + required this.leftButtonText, + required this.rightButtonText, + required this.actionLeftButton, + required this.actionRightButton, + this.alertBarrierDismissible = true, + this.alertLeftActionButtonTextColor, + this.alertRightActionButtonTextColor, + this.alertLeftActionButtonColor, + this.alertRightActionButtonColor, + this.onDispose, + this.alertLeftActionButtonKey, + this.alertRightActionButtonKey, + Key? key, + }); final String alertTitle; final String? paymentId; @@ -57,6 +61,8 @@ class ConfirmSendingAlert extends BaseAlertDialog { final Color? alertLeftActionButtonColor; final Color? alertRightActionButtonColor; final Function? onDispose; + final Key? alertRightActionButtonKey; + final Key? alertLeftActionButtonKey; @override String get titleText => alertTitle; @@ -91,6 +97,12 @@ class ConfirmSendingAlert extends BaseAlertDialog { @override Color? get rightActionButtonColor => alertRightActionButtonColor; + @override + Key? get leftActionButtonKey => alertLeftActionButtonKey; + + @override + Key? get rightActionButtonKey => alertLeftActionButtonKey; + @override Widget content(BuildContext context) => ConfirmSendingAlertContent( paymentId: paymentId, @@ -288,6 +300,7 @@ class ConfirmSendingAlertContentState extends State crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( + key: ValueKey('confirm_sending_dialog_amount_text_value_key'), amountValue, style: TextStyle( fontSize: 18, diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 214d162ed..2a14da305 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -158,6 +158,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin _presentPicker(context), isPickerEnable: sendViewModel.hasMultipleTokens, tag: sendViewModel.selectedCryptoCurrency.tag, - allAmountButton: !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, + allAmountButton: + !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, currencyValueValidator: output.sendAll ? sendViewModel.allAmountValidator : sendViewModel.amountValidator, @@ -257,6 +264,9 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin GestureDetector( + key: ValueKey('send_page_select_fee_priority_button_key'), onTap: sendViewModel.hasFeesPriority ? () => pickTransactionPriority(context) : () {}, @@ -360,6 +372,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Navigator.of(context).pushNamed(Routes.unspentCoinsList), child: Container( color: Colors.transparent, @@ -544,11 +557,13 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin( context: context, builder: (_) => CurrencyPicker( - selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), - items: sendViewModel.currencies, - hintText: S.of(context).search_currency, - onItemSelected: (Currency cur) => - sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency)), + key: ValueKey('send_page_currency_picker_dialog_button_key'), + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), + items: sendViewModel.currencies, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) => + sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency), + ), ); } diff --git a/lib/src/screens/setup_pin_code/setup_pin_code.dart b/lib/src/screens/setup_pin_code/setup_pin_code.dart index 833fd9b60..d78a2df63 100644 --- a/lib/src/screens/setup_pin_code/setup_pin_code.dart +++ b/lib/src/screens/setup_pin_code/setup_pin_code.dart @@ -52,6 +52,7 @@ class SetupPinCodePage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( + buttonKey: ValueKey('setup_pin_code_success_button_key'), alertTitle: S.current.setup_pin, alertContent: S.of(context).setup_successful, buttonText: S.of(context).ok, diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index defc8e2c8..5b7b7f06d 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -133,6 +133,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: PrimaryImageButton( + key: ValueKey('welcome_page_create_new_wallet_button_key'), onPressed: () => Navigator.pushNamed(context, Routes.newWalletFromWelcome), image: newWalletImage, text: S.of(context).create_new, @@ -146,6 +147,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 10), child: PrimaryImageButton( + key: ValueKey('welcome_page_restore_wallet_button_key'), onPressed: () { Navigator.pushNamed(context, Routes.restoreOptions, arguments: true); }, diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index f229ea8ef..0b1ef4796 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -15,28 +15,27 @@ import 'package:permission_handler/permission_handler.dart'; enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses } class AddressTextField extends StatelessWidget { - AddressTextField( - {required this.controller, - this.isActive = true, - this.placeholder, - this.options = const [ - AddressTextFieldOption.qrCode, - AddressTextFieldOption.addressBook - ], - this.onURIScanned, - this.focusNode, - this.isBorderExist = true, - this.buttonColor, - this.borderColor, - this.iconColor, - this.textStyle, - this.hintStyle, - this.validator, - this.onPushPasteButton, - this.onPushAddressBookButton, - this.onPushAddressPickerButton, - this.onSelectedContact, - this.selectedCurrency}); + AddressTextField({ + required this.controller, + this.isActive = true, + this.placeholder, + this.options = const [AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook], + this.onURIScanned, + this.focusNode, + this.isBorderExist = true, + this.buttonColor, + this.borderColor, + this.iconColor, + this.textStyle, + this.hintStyle, + this.validator, + this.onPushPasteButton, + this.onPushAddressBookButton, + this.onPushAddressPickerButton, + this.onSelectedContact, + this.selectedCurrency, + this.addressKey, + }); static const prefixIconWidth = 34.0; static const prefixIconHeight = 34.0; @@ -60,12 +59,14 @@ class AddressTextField extends StatelessWidget { final Function(BuildContext context)? onPushAddressPickerButton; final Function(ContactBase contact)? onSelectedContact; final CryptoCurrency? selectedCurrency; + final Key? addressKey; @override Widget build(BuildContext context) { return Stack( children: [ TextFormField( + key: addressKey, enableIMEPersonalizedLearning: false, keyboardType: TextInputType.visiblePassword, onFieldSubmitted: (_) => FocusScope.of(context).unfocus(), diff --git a/lib/src/widgets/alert_close_button.dart b/lib/src/widgets/alert_close_button.dart index e3ff037a9..6ef0bdaa5 100644 --- a/lib/src/widgets/alert_close_button.dart +++ b/lib/src/widgets/alert_close_button.dart @@ -3,7 +3,12 @@ import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; class AlertCloseButton extends StatelessWidget { - AlertCloseButton({this.image, this.bottom, this.onTap}); + AlertCloseButton({ + this.image, + this.bottom, + this.onTap, + super.key, + }); final VoidCallback? onTap; diff --git a/lib/src/widgets/alert_with_one_action.dart b/lib/src/widgets/alert_with_one_action.dart index 7ad0ac1af..6f0ba5e8b 100644 --- a/lib/src/widgets/alert_with_one_action.dart +++ b/lib/src/widgets/alert_with_one_action.dart @@ -9,7 +9,9 @@ class AlertWithOneAction extends BaseAlertDialog { required this.buttonAction, this.alertBarrierDismissible = true, this.headerTitleText, - this.headerImageProfileUrl + this.headerImageProfileUrl, + this.buttonKey, + Key? key, }); final String alertTitle; @@ -19,6 +21,7 @@ class AlertWithOneAction extends BaseAlertDialog { final bool alertBarrierDismissible; final String? headerTitleText; final String? headerImageProfileUrl; + final Key? buttonKey; @override String get titleText => alertTitle; @@ -45,6 +48,7 @@ class AlertWithOneAction extends BaseAlertDialog { child: ButtonTheme( minWidth: double.infinity, child: TextButton( + key: buttonKey, onPressed: buttonAction, // FIX-ME: Style //highlightColor: Colors.transparent, @@ -62,4 +66,4 @@ class AlertWithOneAction extends BaseAlertDialog { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/alert_with_two_actions.dart b/lib/src/widgets/alert_with_two_actions.dart index ddb11c3ee..e3d4408a6 100644 --- a/lib/src/widgets/alert_with_two_actions.dart +++ b/lib/src/widgets/alert_with_two_actions.dart @@ -14,6 +14,9 @@ class AlertWithTwoActions extends BaseAlertDialog { this.isDividerExist = false, // this.leftActionColor, // this.rightActionColor, + this.alertRightActionButtonKey, + this.alertLeftActionButtonKey, + this.alertDialogKey, }); final String alertTitle; @@ -26,6 +29,9 @@ class AlertWithTwoActions extends BaseAlertDialog { // final Color leftActionColor; // final Color rightActionColor; final bool isDividerExist; + final Key? alertRightActionButtonKey; + final Key? alertLeftActionButtonKey; + final Key? alertDialogKey; @override String get titleText => alertTitle; @@ -47,4 +53,13 @@ class AlertWithTwoActions extends BaseAlertDialog { // Color get rightButtonColor => rightActionColor; @override bool get isDividerExists => isDividerExist; + + @override + Key? get dialogKey => alertDialogKey; + + @override + Key? get leftActionButtonKey => alertLeftActionButtonKey; + + @override + Key? get rightActionButtonKey => alertRightActionButtonKey; } diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 5c1111740..2e6f1571e 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -33,6 +33,12 @@ class BaseAlertDialog extends StatelessWidget { String? get headerImageUrl => null; + Key? leftActionButtonKey; + + Key? rightActionButtonKey; + + Key? dialogKey; + Widget title(BuildContext context) { return Text( titleText, @@ -87,6 +93,7 @@ class BaseAlertDialog extends StatelessWidget { children: [ Expanded( child: TextButton( + key: leftActionButtonKey, onPressed: actionLeft, style: TextButton.styleFrom( backgroundColor: @@ -109,6 +116,7 @@ class BaseAlertDialog extends StatelessWidget { const VerticalSectionDivider(), Expanded( child: TextButton( + key: rightActionButtonKey, onPressed: actionRight, style: TextButton.styleFrom( backgroundColor: @@ -152,6 +160,7 @@ class BaseAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( + key: key, onTap: () => barrierDismissible ? Navigator.of(context).pop() : null, child: Container( color: Colors.transparent, diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index 534e6dae2..4648b88cc 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -30,7 +30,8 @@ class BaseTextFormField extends StatelessWidget { this.focusNode, this.initialValue, this.onSubmit, - this.borderWidth = 1.0}); + this.borderWidth = 1.0, + super.key}); final TextEditingController? controller; final TextInputType? keyboardType; diff --git a/lib/src/widgets/option_tile.dart b/lib/src/widgets/option_tile.dart index 8b46641fb..f7811a888 100644 --- a/lib/src/widgets/option_tile.dart +++ b/lib/src/widgets/option_tile.dart @@ -6,7 +6,8 @@ class OptionTile extends StatelessWidget { {required this.onPressed, required this.image, required this.title, - required this.description}); + required this.description, + super.key}); final VoidCallback onPressed; final Image image; diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index a7cb03a4e..801a79595 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cw_core/transaction_priority.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/currency.dart'; import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart'; @@ -11,6 +12,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/cake_scrollbar_theme.dart'; import 'package:cake_wallet/themes/extensions/picker_theme.dart'; +//TODO(David): PickerWidget is intertwined and confusing as is, find a way to optimize? class Picker extends StatefulWidget { Picker({ required this.selectedAtIndex, @@ -153,6 +155,7 @@ class _PickerState extends State> { Container( padding: EdgeInsets.symmetric(horizontal: padding), child: Text( + key: ValueKey('picker_title_text_key'), widget.title!, textAlign: TextAlign.center, style: TextStyle( @@ -189,7 +192,10 @@ class _PickerState extends State> { Padding( padding: const EdgeInsets.all(16), child: SearchBarWidget( - searchController: searchController, hintText: widget.hintText), + key: ValueKey('picker_search_bar_key'), + searchController: searchController, + hintText: widget.hintText, + ), ), Divider( color: Theme.of(context).extension()!.dividerColor, @@ -203,6 +209,7 @@ class _PickerState extends State> { children: [ filteredItems.length > 3 ? Scrollbar( + key: ValueKey('picker_scrollbar_key'), controller: controller, child: itemsList(), ) @@ -213,6 +220,7 @@ class _PickerState extends State> { left: padding, right: padding, child: Text( + key: ValueKey('picker_descriptinon_text_key'), widget.description!, textAlign: TextAlign.center, style: TextStyle( @@ -242,6 +250,7 @@ class _PickerState extends State> { if (widget.isWrapped) { return PickerWrapperWidget( + key: ValueKey('picker_wrapper_widget_key'), hasTitle: widget.title?.isNotEmpty ?? false, children: [content], ); @@ -260,6 +269,7 @@ class _PickerState extends State> { color: Theme.of(context).extension()!.dividerColor, child: widget.isGridView ? GridView.builder( + key: ValueKey('picker_items_grid_view_key'), padding: EdgeInsets.zero, controller: controller, shrinkWrap: true, @@ -275,6 +285,7 @@ class _PickerState extends State> { : buildItem(index), ) : ListView.separated( + key: ValueKey('picker_items_list_view_key'), padding: EdgeInsets.zero, controller: controller, shrinkWrap: true, @@ -293,10 +304,25 @@ class _PickerState extends State> { ); } + String _getItemName(Item item) { + String itemName; + if (item is Currency) { + itemName = item.name; + } else if (item is TransactionPriority) { + itemName = item.title; + } else { + itemName = ''; + } + + return itemName; + } + Widget buildItem(int index) { final item = widget.headerEnabled ? filteredItems[index] : items[index]; final tag = item is Currency ? item.tag : null; + final itemName = _getItemName(item); + final icon = _getItemIcon(item); final image = images.isNotEmpty ? filteredImages[index] : icon; @@ -316,6 +342,7 @@ class _PickerState extends State> { children: [ Flexible( child: Text( + key: ValueKey('picker_items_index_${itemName}_text_key'), widget.displayItem?.call(item) ?? item.toString(), softWrap: true, style: TextStyle( @@ -335,6 +362,7 @@ class _PickerState extends State> { height: 18.0, child: Center( child: Text( + key: ValueKey('picker_items_index_${index}_tag_key'), tag, style: TextStyle( fontSize: 7.0, @@ -358,6 +386,7 @@ class _PickerState extends State> { ); return GestureDetector( + key: ValueKey('picker_items_index_${itemName}_button_key'), onTap: () { if (widget.closeOnItemSelected) Navigator.of(context).pop(); onItemSelected(item!); @@ -383,6 +412,7 @@ class _PickerState extends State> { final item = items[index]; final tag = item is Currency ? item.tag : null; + final itemName = _getItemName(item); final icon = _getItemIcon(item); final image = images.isNotEmpty ? images[index] : icon; @@ -390,6 +420,7 @@ class _PickerState extends State> { final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex; final itemContent = Row( + key: ValueKey('picker_selected_item_row_key'), mainAxisSize: MainAxisSize.max, mainAxisAlignment: widget.mainAxisAlignment, crossAxisAlignment: CrossAxisAlignment.center, @@ -402,6 +433,7 @@ class _PickerState extends State> { children: [ Flexible( child: Text( + key: ValueKey('picker_items_index_${itemName}_selected_item_text_key'), widget.displayItem?.call(item) ?? item.toString(), softWrap: true, style: TextStyle( @@ -445,6 +477,7 @@ class _PickerState extends State> { ); return GestureDetector( + key: ValueKey('picker_items_index_${itemName}_selected_item_button_key'), onTap: () { if (widget.closeOnItemSelected) Navigator.of(context).pop(); }, diff --git a/lib/src/widgets/picker_wrapper_widget.dart b/lib/src/widgets/picker_wrapper_widget.dart index f4e52c5cd..ac863ac5d 100644 --- a/lib/src/widgets/picker_wrapper_widget.dart +++ b/lib/src/widgets/picker_wrapper_widget.dart @@ -4,7 +4,12 @@ import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/src/widgets/alert_close_button.dart'; class PickerWrapperWidget extends StatelessWidget { - PickerWrapperWidget({required this.children, this.hasTitle = false, this.onClose}); + PickerWrapperWidget({ + required this.children, + this.hasTitle = false, + this.onClose, + super.key, + }); final List children; final bool hasTitle; @@ -29,8 +34,8 @@ class PickerWrapperWidget extends StatelessWidget { final containerBottom = screenCenter - containerCenter; // position the close button right below the search container - closeButtonBottom = closeButtonBottom - - containerBottom + (!hasTitle ? padding : padding / 1.5); + closeButtonBottom = + closeButtonBottom - containerBottom + (!hasTitle ? padding : padding / 1.5); } return AlertBackground( @@ -46,7 +51,11 @@ class PickerWrapperWidget extends StatelessWidget { children: children, ), SizedBox(height: ResponsiveLayoutUtilBase.kPopupSpaceHeight), - AlertCloseButton(bottom: closeButtonBottom, onTap: onClose), + AlertCloseButton( + key: ValueKey('picker_wrapper_close_button_key'), + bottom: closeButtonBottom, + onTap: onClose, + ), ], ), ), diff --git a/lib/src/widgets/primary_button.dart b/lib/src/widgets/primary_button.dart index 5f6b50f8b..06bfda157 100644 --- a/lib/src/widgets/primary_button.dart +++ b/lib/src/widgets/primary_button.dart @@ -4,15 +4,17 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class PrimaryButton extends StatelessWidget { - const PrimaryButton( - {required this.text, - required this.color, - required this.textColor, - this.onPressed, - this.isDisabled = false, - this.isDottedBorder = false, - this.borderColor = Colors.black, - this.onDisabledPressed}); + const PrimaryButton({ + required this.text, + required this.color, + required this.textColor, + this.onPressed, + this.isDisabled = false, + this.isDottedBorder = false, + this.borderColor = Colors.black, + this.onDisabledPressed, + super.key, + }); final VoidCallback? onPressed; final VoidCallback? onDisabledPressed; @@ -31,23 +33,23 @@ class PrimaryButton extends StatelessWidget { width: double.infinity, height: 52.0, child: TextButton( - onPressed: isDisabled - ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), + onPressed: + isDisabled ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed, + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(26.0), ), ), - overlayColor: MaterialStateProperty.all(Colors.transparent)), + overlayColor: MaterialStateProperty.all(Colors.transparent)), child: Text(text, textAlign: TextAlign.center, style: TextStyle( fontSize: 15.0, fontWeight: FontWeight.w600, - color: isDisabled - ? textColor.withOpacity(0.5) - : textColor)), + color: isDisabled ? textColor.withOpacity(0.5) : textColor)), )), ); @@ -64,13 +66,15 @@ class PrimaryButton extends StatelessWidget { } class LoadingPrimaryButton extends StatelessWidget { - const LoadingPrimaryButton( - {required this.onPressed, - required this.text, - required this.color, - required this.textColor, - this.isDisabled = false, - this.isLoading = false}); + const LoadingPrimaryButton({ + required this.onPressed, + required this.text, + required this.color, + required this.textColor, + this.isDisabled = false, + this.isLoading = false, + super.key, + }); final VoidCallback onPressed; final Color color; @@ -88,41 +92,38 @@ class LoadingPrimaryButton extends StatelessWidget { height: 52.0, child: TextButton( onPressed: (isLoading || isDisabled) ? null : onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26.0), - ), - )), - + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26.0), + ), + )), child: isLoading ? CupertinoActivityIndicator(animating: true) : Text(text, - style: TextStyle( - fontSize: 15.0, - fontWeight: FontWeight.w600, - color: isDisabled - ? textColor.withOpacity(0.5) - : textColor - )), + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w600, + color: isDisabled ? textColor.withOpacity(0.5) : textColor)), )), ); } } class PrimaryIconButton extends StatelessWidget { - const PrimaryIconButton({ - required this.onPressed, - required this.iconData, - required this.text, - required this.color, - required this.borderColor, - required this.iconColor, - required this.iconBackgroundColor, - required this.textColor, - this.mainAxisAlignment = MainAxisAlignment.start, - this.radius = 26 - }); + const PrimaryIconButton( + {required this.onPressed, + required this.iconData, + required this.text, + required this.color, + required this.borderColor, + required this.iconColor, + required this.iconBackgroundColor, + required this.textColor, + this.mainAxisAlignment = MainAxisAlignment.start, + this.radius = 26, super.key}); final VoidCallback onPressed; final IconData iconData; @@ -144,7 +145,8 @@ class PrimaryIconButton extends StatelessWidget { height: 52.0, child: TextButton( onPressed: onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(color), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), @@ -158,21 +160,15 @@ class PrimaryIconButton extends StatelessWidget { Container( width: 26.0, height: 52.0, - decoration: BoxDecoration( - shape: BoxShape.circle, color: iconBackgroundColor), - child: Center( - child: Icon(iconData, color: iconColor, size: 22.0) - ), + decoration: BoxDecoration(shape: BoxShape.circle, color: iconBackgroundColor), + child: Center(child: Icon(iconData, color: iconColor, size: 22.0)), ), ], ), Container( height: 52.0, child: Center( - child: Text(text, - style: TextStyle( - fontSize: 16.0, - color: textColor)), + child: Text(text, style: TextStyle(fontSize: 16.0, color: textColor)), ), ) ], @@ -189,7 +185,7 @@ class PrimaryImageButton extends StatelessWidget { required this.text, required this.color, required this.textColor, - this.borderColor = Colors.transparent}); + this.borderColor = Colors.transparent, super.key}); final VoidCallback onPressed; final Image image; @@ -206,31 +202,27 @@ class PrimaryImageButton extends StatelessWidget { width: double.infinity, height: 52.0, child: TextButton( - onPressed: onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26.0), - ), - )), - child:Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - image, - SizedBox(width: 15), - Text( - text, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: textColor + onPressed: onPressed, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(color), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26.0), ), - ) - ], - ), - ) - )), + )), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + image, + SizedBox(width: 15), + Text( + text, + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: textColor), + ) + ], + ), + ))), ); } } diff --git a/lib/src/widgets/scollable_with_bottom_section.dart b/lib/src/widgets/scollable_with_bottom_section.dart index e15be610e..07a070204 100644 --- a/lib/src/widgets/scollable_with_bottom_section.dart +++ b/lib/src/widgets/scollable_with_bottom_section.dart @@ -9,6 +9,7 @@ class ScrollableWithBottomSection extends StatefulWidget { this.contentPadding, this.bottomSectionPadding, this.topSectionPadding, + this.scrollableKey, }); final Widget content; @@ -17,6 +18,7 @@ class ScrollableWithBottomSection extends StatefulWidget { final EdgeInsets? contentPadding; final EdgeInsets? bottomSectionPadding; final EdgeInsets? topSectionPadding; + final Key? scrollableKey; @override ScrollableWithBottomSectionState createState() => ScrollableWithBottomSectionState(); @@ -35,6 +37,7 @@ class ScrollableWithBottomSectionState extends State { top: 10, left: 0, child: Text(S.of(context).enter_seed_phrase, - style: TextStyle( - fontSize: 16.0, color: Theme.of(context).hintColor))), + style: TextStyle(fontSize: 16.0, color: Theme.of(context).hintColor))), Padding( padding: EdgeInsets.only(right: 40, top: 10), child: ValidatableAnnotatedEditableText( + key: widget.seedTextFieldKey, cursorColor: Colors.blue, backgroundCursorColor: Colors.blue, validStyle: TextStyle( @@ -112,15 +116,17 @@ class SeedWidgetState extends State { width: 32, height: 32, child: InkWell( + key: widget.pasteButtonKey, onTap: () async => _pasteText(), child: Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).hintColor, - borderRadius: - BorderRadius.all(Radius.circular(6))), + borderRadius: BorderRadius.all(Radius.circular(6))), child: Image.asset('assets/images/paste_ios.png', - color: Theme.of(context).extension()!.textFieldButtonIconColor)), + color: Theme.of(context) + .extension()! + .textFieldButtonIconColor)), ))) ]), Container( diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 54bd54f53..98661f7c1 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -107,11 +107,14 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter + mocktail: ^1.0.4 build_runner: ^2.3.3 logging: ^1.2.0 mobx_codegen: ^2.1.1 build_resolvers: ^2.0.9 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 # flutter_launcher_icons: ^0.11.0 # check flutter_launcher_icons for usage pedantic: ^1.8.0 diff --git a/test_driver/integration_test.dart b/test_driver/integration_test.dart new file mode 100644 index 000000000..2e67d866f --- /dev/null +++ b/test_driver/integration_test.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:integration_test/integration_test_driver.dart'; +import 'package:path/path.dart' as path; + +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + integrationDriver( + responseDataCallback: (Map? data) async { + await fs.directory(_destinationDirectory).create(recursive: true); + + final file = fs.file( + path.join( + _destinationDirectory, + '$_testOutputFilename.json', + ), + ); + + final resultString = _encodeJson(data); + await file.writeAsString(resultString); + }, + writeResponseOnFailure: true, + ); +} + +String _encodeJson(Map? jsonObject) { + return _prettyEncoder.convert(jsonObject); +} + +const _prettyEncoder = JsonEncoder.withIndent(' '); +const _testOutputFilename = 'integration_response_data'; +const _destinationDirectory = 'integration_test'; diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 3084af07c..d3b652935 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -39,6 +39,7 @@ class SecretKey { SecretKey('moralisApiKey', () => ''), SecretKey('ankrApiKey', () => ''), SecretKey('quantexExchangeMarkup', () => ''), + SecretKey('seeds', () => ''), SecretKey('testCakePayApiKey', () => ''), SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), From e2b3507134371487e923662142863c2a59a4e9c0 Mon Sep 17 00:00:00 2001 From: cyan Date: Sun, 22 Sep 2024 23:16:09 +0200 Subject: [PATCH 086/142] CW-745 don't show txkey when it's unavailable (#1692) --- cw_monero/lib/api/transaction_history.dart | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 7723b6051..bd8b46356 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -319,27 +319,9 @@ class Transaction { final txKey = monero.Wallet_getTxKey(wptr!, txid: monero.TransactionInfo_hash(txInfo)); final status = monero.Wallet_status(wptr!); if (status != 0) { - return monero.Wallet_errorString(wptr!); + return ""; } - return breakTxKey(txKey); - } - - static String breakTxKey(String input) { - final x = 64; - StringBuffer buffer = StringBuffer(); - - for (int i = 0; i < input.length; i += x) { - int endIndex = i + x; - if (endIndex > input.length) { - endIndex = input.length; - } - buffer.write(input.substring(i, endIndex)); - if (endIndex != input.length) { - buffer.write('\n\n'); - } - } - - return buffer.toString().trim(); + return txKey; } Transaction.dummy({ From 6592b7a3c5276de50705a6213d91887c49fe7f6a Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Mon, 23 Sep 2024 10:10:40 -0400 Subject: [PATCH 087/142] Generic enhancements (#1668) * Add more choices for pin code required duration * Fix spacing in Cake Features * Update Cake Features cards * Update Cake Pay image * Add NanoGPT image * Update Sign/Verify strings * Update more flags * update moonpay currency name * Update more icons * Add friendly message for less than minimum fee * fix translation [skip ci] * Fix icon theming and add Telegram link * Fix color issue for restore screen * rename matic poly to pol * minor potential fix [skip ci] * minor fix [skip ci] * Update analysis_options.yaml * fix translations * fix translation * revert regex changes * Update bitcoin_cash_electrum_server_list.yml * Update address_validator.dart --------- Co-authored-by: OmarHatem --- analysis_options.yaml | 3 +- assets/bitcoin_cash_electrum_server_list.yml | 9 +- assets/images/cards.png | Bin 0 -> 47094 bytes assets/images/cards.svg | 65 ---------- assets/images/flags/arm.png | Bin 4846 -> 367 bytes assets/images/flags/bra.png | Bin 692 -> 1330 bytes assets/images/flags/hau.png | Bin 1071 -> 4217 bytes assets/images/flags/ind.png | Bin 845 -> 975 bytes assets/images/flags/ukr.png | Bin 695 -> 366 bytes assets/images/nanogpt.png | Bin 0 -> 516852 bytes cw_bitcoin/lib/exceptions.dart | 2 + .../lib/pending_bitcoin_transaction.dart | 4 + cw_core/lib/crypto_currency.dart | 2 +- cw_core/lib/exceptions.dart | 2 + cw_ethereum/lib/ethereum_wallet.dart | 2 +- lib/buy/moonpay/moonpay_provider.dart | 4 +- lib/core/address_validator.dart | 116 +++++++++--------- lib/entities/pin_code_required_duration.dart | 27 ++-- .../simpleswap_exchange_provider.dart | 2 +- lib/polygon/cw_polygon.dart | 3 +- .../dashboard/pages/cake_features_page.dart | 24 +++- .../screens/restore/restore_options_page.dart | 25 ++-- lib/src/screens/support/support_page.dart | 31 ++++- .../support_other_links_page.dart | 1 - lib/src/widgets/dashboard_card_widget.dart | 5 +- lib/src/widgets/option_tile.dart | 11 +- lib/store/settings_store.dart | 2 +- lib/utils/exception_handler.dart | 4 +- lib/view_model/send/send_view_model.dart | 3 + lib/view_model/support_view_model.dart | 11 +- res/values/strings_ar.arb | 5 +- res/values/strings_bg.arb | 4 +- res/values/strings_cs.arb | 4 +- res/values/strings_de.arb | 4 +- res/values/strings_en.arb | 3 +- res/values/strings_es.arb | 4 +- res/values/strings_fr.arb | 4 +- res/values/strings_ha.arb | 4 +- res/values/strings_hi.arb | 12 +- res/values/strings_hr.arb | 4 +- res/values/strings_hy.arb | 10 +- res/values/strings_id.arb | 4 +- res/values/strings_it.arb | 4 +- res/values/strings_ja.arb | 4 +- res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 4 +- res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_vi.arb | 14 +++ res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + 58 files changed, 276 insertions(+), 194 deletions(-) create mode 100644 assets/images/cards.png delete mode 100644 assets/images/cards.svg create mode 100644 assets/images/nanogpt.png diff --git a/analysis_options.yaml b/analysis_options.yaml index 2063b5a8c..be68a4f26 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,5 +1,6 @@ include: package:lints/recommended.yaml + analyzer: exclude: [ build/**, @@ -83,4 +84,4 @@ linter: # - unawaited_futures # - unnecessary_getters_setters # - unrelated_type_equality_checks -# - valid_regexps \ No newline at end of file +# - valid_regexps diff --git a/assets/bitcoin_cash_electrum_server_list.yml b/assets/bitcoin_cash_electrum_server_list.yml index d76668169..948e5f3dc 100644 --- a/assets/bitcoin_cash_electrum_server_list.yml +++ b/assets/bitcoin_cash_electrum_server_list.yml @@ -1,3 +1,10 @@ - uri: bitcoincash.stackwallet.com:50002 - is_default: true \ No newline at end of file + is_default: true + useSSL: true +- + uri: bch.aftrek.org:50002 + useSSL: true +- + uri: node.minisatoshi.cash:50002 + useSSL: true diff --git a/assets/images/cards.png b/assets/images/cards.png new file mode 100644 index 0000000000000000000000000000000000000000..b263bc7425fe897ff3fe16897e684153ae7a7340 GIT binary patch literal 47094 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�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 literal 0 HcmV?d00001 diff --git a/assets/images/cards.svg b/assets/images/cards.svg deleted file mode 100644 index 699f9d311..000000000 --- a/assets/images/cards.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - diff --git a/assets/images/flags/arm.png b/assets/images/flags/arm.png index 7bdabd4ee76f5e86f5aa3c8722a2867cabe683dc..0e4c356e24d85169b059671b66f921e52de5ee65 100644 GIT binary patch literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|TR? zr;B4q#hkZS8F`Nw@Hk%-SlzYojaSN#A`LbF>EBg_H_Y7uG86(NT-Rr^N~1&G3?Id|ai5UB7#DDlM|#7vlN|btN1|`& zDdf9UJ60O!9GIo9yvI)NfSv#4&$HN>EN(V&KHy0B(ycB1fkWZ$A&_A?`WpFdycVBR zTKX9t>D~cqJ@T#TQ`(INV}G|F918c)6rDfL{36=-E6}hVAj7!8h6ONrY~K*;aIXWR z(;BFg3#hY;5%KKOGYPYKk^~Y|gvFIA zzN?m2DPpVCg7^Zpt8Enl5sR-y>hV#ndd|YOqQ}RsRz&wsc<=U{J)X1uk8p18H~0R& z-~I0Ie)of6T|!*+XfJ;+2!ci{Vxkg3KN~cE<}mPG^VduN0^JodRiRTTWGlshR4Z!|Gx4j+MA>A0-hsW@MMuWZ>G(NC> zL0=9U^7y0O1QwseLpTC4@JIM4hKMm9%Hks!ieh{etj{Rz zi-=}=vw60*e8E{HB{;Uk?+Llnqxox)SzjpsaK~APl*`!8p_DBCOWqIye;xwT<5ah z-t2ra#rYV@t-7G93TEuzNfL>QprU1duaoJzagpp_8p`C(pC7X}DSqyuNUOh&UEbII6lUJTguFqh48LmUPfI~C+n zq{T+EP!7sLV7ZIR;Oq-UJXOU*dwEh%?w%6a9X*0UF zLuk3Aku(9Q9jwY5uq1usdOc_f^pwf!_5xxLKsqR0pI8H8qi5XP=^qGi@8unU?%BH= z3{XlX7G=?9(%~thWNdnUjId}a0&|-@F@f_?5ef?ggak(UJOUQ;a0C_!gd#+PkOBdY z_oGsn?GD_mC21-^&Y=L0MkLXaJYguT6Z3R{LL`7CT2umSg+f9r2$kSsT-T2x-bR6} z#Et!aQK7MsdqG0NDzRkwFq39b`? zJMbvnK?2g=I;`rc!*dWmhIEBb(`wLpLmb{@h}*WH_DIITJ?aO z$no;`4V%92xaK#iK<;Tj4abOUkL1V;BA%R;sJ5(q;IiDSX%o$SU1q8&-eClp3Mu4q KQ3n@%kpFL7qruVu diff --git a/assets/images/flags/bra.png b/assets/images/flags/bra.png index ee0b0e8c6ff274d6dc5486cae634698ae73df233..6c4ba8968f3350dfbe982016979da6492e46386f 100644 GIT binary patch delta 1326 zcmV+}1=0Gn1+oeuiBL{Q4GJ0x0000DNk~Le0001B0001B2nGNE0OFW;IFTV2e+1-7 zL_t(|ob8(bPg7SI#`(YOvM`qo>jfEXq3i;S%7VBb=F}~aWy!u|Nq@j(*`@{**ucd_ zjn^;{i;l@!CL?QQGH5~F&_TskiH6K7us+MV8H-~w?w51!Ik&t|@+PEB$?5q#&wI{2 zr$t315{X12kw_#Gi9{liNF)-8e?%gYNF>sRl4g{Hs$AF{5CB82!3cms05)`sE21@^ zoB+snp+*UCKMDHTGDMTlVPvuxb-iWMCw4=%pm1~x?x&vtvYX6WxE;XfmrJCNtKb3X zf4hEV;#Zd+(%@SF;Z?3*t5i>e?*QoQxZZDR@D+ftMc4Z&4ZZ-NJ9NDtf6|~lfUrx~ ztDXjB0qC4vuWA~U1P~VFdKJ^46ad|Z>+_iaCV#3#`_X2+*>o5+ja_&#bjGaO#xC=< zj`n6u#w(OggE9cDx;~!?V4$xaHM_d;>`T3tiov-QsW(5XS{kGSu;cpK2@SjU9nulg(iumd0o8<7jw0hT@8I>jGfU3{J+!D-FT`xP6+v znR*v)b$9*|jJ|2?d2blSm21P8N>n-?r`t4e2f*tLCmz2Dp32_Kq3u`-rM3P40npje z#ic+07sR8- z%jo=i3NKWhH;*cKj@?cgTmukwa#<2GTx~J8*7W1!Peu{@e{l|v7uSZ7p8EqIvi7UGE71w`s^{ z0_OHmG=RF@UwZC(&j7eh!=*^QZ~));Z};5wo&#{2hI}SqUL%T{{5STu+CGlv;d&_m zoTlM+YOAmd8Ft4k!}U@DI7!3kP_=oD$ZPW1|N8lde~>iSO9$X24P(Pq+%DzW9kWc= zD+9o88q&$2d5!3%=d+e3+x1ETu$zW_CSYD8vYRYBTg}194rN@gBmlcpxBO%Js?u&}^oGS9$GvkE%)Pm$s2CV_c#;bJ~VG@Z%B9TZW5{X12kw_#G ki9{liNF)-esF3s*b9SXICZlBl00000NkvXX1g=70g5JD%hX4Qo delta 683 zcmV;c0#yC73bX|wiBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!{e*$Jn zL_t(|ob8tHOH)x8$It&&lKG=lE^0$!B?S2ce5G&1*j@xmSug5M1O;t_1YhdJ-vjXn zn@B2(ELhQKC0T~GU09Jx@s;P&t<-z(Id}JLI5}T}K*@$?m3ahU#f8xeeV|fAGkJm&0SOM&vGb;b-JRYyqoqmAUKdzex zKVCA^V6p*@yKYyq(qOUxc&-;K4JHG?%JuBqO9(v+qP;tU%ltO3;?Gz z2!fWBlk--Gj|;jk4ZtZ4+OV{wI(s6#D+U1XZg}l= ztyBOaX?S&AD>i^#8nRs1N(J!$A3>R}Yo!Dzb}7V5Q R1ONa407*qoM6N<$0fIcNHp&11 diff --git a/assets/images/flags/hau.png b/assets/images/flags/hau.png index 7583b5daf03e5848bef2c8c93cd818fb71c766a2..2bfb0a71c0faf730cf994008dba41da448a2bbd1 100644 GIT binary patch literal 4217 zcmb7I_ct4k7fnKt)LyOH>m$aerS@pd5Q(qI^Cm{t8Jk`Y%SeicTM&Z>}Y2(p7wf-?`&OM5DN*1Vg$ zQTTQ2-<-cW!Z-R0e-LZcYpvdC?t7uV_eenhZ^es(%z5SGGFsTzs7`-3`uh4l7P`2y zhEnef;vr{`FOuRfJ_=%K*$e{9zZ_;jB42;hEIB)8-g9CYl0)lnUv-loxNGX zR6p}O1T3ej%syFnL1!DCb4i!nuSaRLJMZ%%suDtL@_N^<|1LEZJ$H4Kbb#lNZdbqkN&eCGi12H+LG+eKc8V zq}-qOu*9Y(jz2kkt?T>uk=NBNs_N?MJWS=Xu3o!83BfA!GHh`F-sFF@tTPVgKfBtn zNjk9%r;CKD#oqhoxiz_BT#r8J2)}djEtFJZQP}~0wmuumACmr8fL2wm=0<_>g~O4* z>&WI`ar8JLf`a+b#JQ#Gqy6CWXn~r$iVE?q|5@_q1fp)thOma@g&&NN{1m=~o+x*9 zb9Qy4bPdXoAv-@(F{YU! zufk_UihkHnrq&PM+uIw4Kp_6Vdududw;1~cp7j){#k=kOY)N&4Uv7)C?t3eEG?@Tz z75t)Pm8lU5fP3#|4^qKhmx;YMeuQ^P93}BSxhHDKoWAm_JJ#lqh7cJebk`U0Jxglm zT8xA+OZ~C?QSd_31c|%hCQe^n59jJtZR;Qb%0zTkVN$ zfk8-h^+Rl~We|kUb)TR}Nk#D@hHm7>$sD+cujO>rM3Lt{#uzL>W8TZZfn$}Q6JEE? zf$nMq91TiJ%g7jiP>`e&1g~d8?g;lSO_u3J3OsMvyP+A4g5b{2eeO%U@kGzy1fN%p z+zMJ(`U}QQE{|O$l3-DEiEm!4*^h1NS8SwN1m?%X|3r1iGATc7Ii`)~Dm8EM^UC4) zseBdWHel?wmfhoD__6Ylm01BtSf({C(w+w&+rV5;7_(Is;0tGS31PDue?V-No-MBZr+$Zc`b&Sbl9A(EdW^zJSjs)TX2d*@&qb{cd)V6T{X{t zA-1}2X>1$uF)UR3F%+J!SE8LRt*OYxt-=^nh#e4Szl(Q`LfX+tKNJy3|d0E_TZs-jTzk59(+ia$DGO~ta< z)H~?srIye7xmJzMD-1rADu$B8Icd$z%!s)zt0YAsSJ-UH7F9MSPxUCK4q!w6w^>QquV-T!( zJyj*}`+C2V>ZFfIWBBw~2cV;f3OzTid&&?whkZ6#ZBacCI`C4}mPZV9wmqpAo^xE+ ztC>hlcYdApr|gmi?w`(EU~eQX)O9~gL&|eA1;oxNa7fXwGX*|c9w=H-2KwmztDXc? z;m@}<2ka-7NubB0tV-%#ug+a}lSd-)PlMta4tTYFrHc%++kB7gDXFN|><2Rs8XV_h zfCb^7Qw8*wyRdgncrinH>mAZ7YQS?c+XDBsA$`$W%-JqNP|=6WPBo!MeWiaBBho)t z?^t3V*s~_%rtPI&>z=UTH2W!<_X-8Q+N`J^Uj)KgWZEs7Jj#9&elhf=f%a9ShV zIt?L@{f;eNhgfK7JBH>|#PHNRR4@`YQ*>Xd6G6G?wcF zT3;Og#tVQ#>wv9s_C1HpeYv20uiHeI3%v4Kp(A9|(9S}+X}X89B;x}D{4Fh81d&Ba*+UR*J-1dqr#bG=6&wRc^1JeG-y}GT&E)yHx-YY65gt<)$&r04%^wlC{-BO{ef}<_y z@WG0%fjy(+VpC4*;Q6~lr&StG3qOOxGO`#wWcgq2y117LZ9uV$1^lOrtij{c^~MdS z*nO(&6=%*>TTM)CwKRcO}rBV2`0BEotj05 z06ycM{%M3Qttp^KE5QMcH?p|hLlaV(w~|U{fl z85vb+$zgG&cKz?h8yrp2#PhKrOK&}r!jZL`Kl6XWfnu#h4)$S8+stmn`Ndk`N_j_K zA&F4p4%-x@4NbPQ;JbFuV6F_VJA?7v<@X%|DGlDOh}NLnOP9~|xG*YSS&K#Av#8kN zrZku;N)sr|U4p)1`((<3FM5pOMCkNAPah{9&cBADG@IcQB*gkdk$v{|2!KmJ10dXi z4$rW4y#4Xh4Ixlq0EVJ$?7cD+01}9F25gO_h1mzQ#7p+5{GA_(?;!Sz2r)v>0V$s2 z{$_Ap!wVT!L| z&)Z4GXo9)g4_-t?Q6U4RGm4vK!&_&LDc<^G$QQ5-gHngUH6s21ucmTd&3p2}lTN0u z4}q)*%bx@yO6)fMg^`dI^6}<)ZSo;dD^pSz4k+e>!Ai$7y9!|6C!sY>@q*?*XI>Y* z?6^GJU3-;W;`*z%J4TzD&mm@uPD?i-$RgzqN2^+QC5{8tsiCTv{JRlm{l?n1(ho%wr5YuBqP@`)u|_-n?0h%Ye}m3xVb~KB%Q0GTKM&fVbJ?i`E#HhVHv85 zYbyx`u7^cgAOJkGaD*@H5zgT5`SI9O1_laqc8@x{H`GuW`M>AfjjzU3`2mg(GBPr{ z?tcO7HhFHv>>k{1oM5F@oVou2VuCMBCWw+3`Ghs?A=vVt0%ua8Qr`1fW+DZTy5OV+ zTZArNV>9P_Qga4N`SfWpo(Rj}DSV`ekbLz+MFjVbXsQ4mv4&eIvOP*;evY`%4I+W} z@~5+6wDfdBzW?fD31$WF5Iw~MuP+XAjLv6g_ZRCGhMusX_GGbDyn3ruO7Bt z)s5q0sZTD}a$iKB>DvvMKSVQCMIGh?0NgW0pmB4IH9lT-Efd6cAZMr*B(Fy70E>eS zWIYEBWyyG!>wV6bq>p0)bEGIN$T@x0k(~T2HWsan#@oR4RDv8(@7(C5#BI^A)Im?+LS{6Dk0{Ovmy#BrmKr z;8Ng}ad-@~UJf{Rw0@`Vp@%l%QlM5VL$u&@~yrSe^qf%i$T;lH9*JWgdcdRomxxrlesSJBh`bJ(%5 zA6Q+Q-mpX~?hQ~qaHx!@KEJ-%<{1ZzmncGaTYjVF>kndvMt(q4!2~sm$g7uwjkJ_w zcBCYp{BDK-jy5cU*F;N; zFv$R*Zt(|6Y3Qf3AN2J?mmvCpmf!48G_(pW(ZZdhggd+nc&X(@nF0SDcL#xX0|YtW zCM_%HVS9rv%V=~^!bnCy2eYI@Vc}nOlut>5Qi$@)g;~$>Dl^$gAh|{%nPh!HhtK=C z%P@B62@X7r*fU-gw~B-rW(Upgj6IdV#yrqniBUlk1cIn*fcY5#AmnVYTbLA!K2r#uJiBf)t~ zjClp5khcwf;!{n^(GA%y8K)6VzlvO+LaZj<6VzOncl}D#zEpn@Ms2ZUQZAXMrqu|+ zm8c4c1*g`?uR=SMf51Lxts6!W#EpAVQgp zdNXaTp^(F|6zN2V1nP_1**p|!(|P8J?L50^^ZtJKLXyly-WA)6`o8(|e0l!A-{=2( z9)ADlffssCWjs8PGzq*8%>O?)%X$?E7>4o217qF4OnCSiv+C;V2!%qNK7Bgg5Rb9| zt3jVygM))iO-()Pu(cy&h6a2- z9{?2<73|)<`{}49h4lnQA`u-tcu*}ZEt;E~)3Ia6w15A8Nz#~SOniv=0QG&>CrK(T zEmd`OwPt2!=h1c2Hsp^VqZOPsuzCv6=sX3{t zbXBUgwROeZs;Vk=b#=uIYKgV#IXO9GW@h4YxiC!=fP#Vo0+Rv8M#kVSFblW;xXsPs zn;bZBVENt3%1W$OD|`3ujWuoT6@8$F3k|A0S*zf!pejD8Q1kcAlB8|v+mvV|N|HJ} z9debp6ucGG`LpNcKI>j<_*E5+8&r~t@{0th=|a;ISW}0{L%fTc>X*FhZPx{8*N$D% z-?2nr3HoTm5A&EF+SAiRS7#Svqhol(Ui`24F*8lD!_HrJlKOip*Iv2?<$w*i1O?#& zB54u&a{AyeFh&is?q;#Wwu7v#S?qal5Bv7*dtBY-4Z}!&Fj#YYibNtzPfrt^4ibn2 zFry|U0$l=|69=d`g%DW4uwLYWv}Gp5^YT1C_Y1+ zMC;#Nuflf<6^TS7Nv3J)(4j**e*Ab`pfUGRzu!-Pe?RT*?WCln0O0fauqD}+_H7!l z749$GXG6*cyk758B8!D9SFSKKGedWGcdTh+ujuUTtej4#dV6~%Np81Wd3kw?no$jR z42uFpA@Y3Vkt7ur73su@6EaOx!^6XJI-T*w{!?QWD9@$s9d; z6uaFHK>FM1j8BfEQGpGj(I{umoMCEeihK9&QC3z)et!O1>b9ai#RUqmxVXsT;v(zT zt?Y0vmy19kKyPpF3O^c+Vi*Qit932mSAoWT9<^93SS+izcy4Ykn>TM>t@eC#YS`9J4pfB!N@poPfbf#!)T91HhG z@+mZEMkfdyXknO_S14uU(9Cct)Z+%@r%r_}3O`d0F#Eh`^|{f)@Q6skHN}iMx7Awy zrdc*HY|Cl1jAJ;xYEq_{hQNt{>fbF~b$!RVQzAt_e%%?>kWw^VO4@zJ;^awfZQG^@ zEMClN)T4XpD`!?#SCH0L^?iQfToY>h*LUAOHH7j~-3a)6akT;6YTc zuFjl*;^H2sg+SwWc>#?JSKSt~Z{czG+1IY^yZq-5=iB%3fg3B<3Y|~>)A#(tovOQ> zeZmJety=ZA;AwHw@?~CIHWn`X=eF3|#AuWA&78WW-u-2ijIOJ!a`R&i&+j z{erBojh}uR-@0S7_~1d`t7%OKUAfq%Uoq~_&7Z&8vb_BJlkfgPN*lHJ)g9-)aCPc3 zj}-5(KV#mt7|q<|RbjEmY46_CT0iRhN_MSz_95mU@BF?c*WXt#MnxajoT#C(Syya* zM#T@~uU~Xl9x*rB6MXQz&9S!~&p*W6v#fEM8n^#3Yeu$o_{lY#2R#49H~jl2YI^L^ z6_y9eTJpY2LFO(84vhPE~~m+ssV>|3~gCwI`wz2fpouU~KF-x$B@u8!{9RfcupT6ZU$7hk+_ z2r)?xZyv9IjGk?ZS}end4L|1tOc_j!jNh5}>BkKuQw`iF4CYA2OX zp0P)cCe=uO|M=5rU7YrPQKlRI^9~&Q^U2C;Ezfqj^>Q)l4R1d*FqF;ZULZZUoJvH*@>JX!0e2?N9GM%l6re5t(O7`c)I$ztaD0e0sxt!w;%uj delta 837 zcmV-L1G@ar2h9c{iBL{Q4GJ0x0000DNk~Le0000g0000Q2nGNE0KvJ=_K_hMe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP0008wNkllu{hB*;5R!{eQ+99%=Y&U1AC#!$lN}MKPUD zMLwUWMx&wnppVC6(dl%g?~l>-^|hMg{QO*M`}uq>PESvz?#DQ^6$%B3dC-nF@4Q-X zHk;IHwVYC^BrBB)=5V}3LC{<-myuX3X0BE%v0kr5JRS#W^@GqOk%%0Pe@65#_{V7d zrFNEON!FcH#R^<+ zuopQ#BB3A&um>}|dM2;p26aPsa7>vLGI%)|6mnn_Lb^l|#1t|NheOh*hf5T@oI-{~ zBH?nfc6#1?pfKV2`Kxnwe|DzyU82P@1wpm?n|i$od*Xz0xfEry2?~N_@;lCRrK_da z0UHF}?u2Z6NjEoFiX`eFek>@WI$(nUq(JcQ?vYEKy>X(*9t%f3p5RdswCx4U7##z8 zwcQxSQ4j=(M-aTdeWwUNJq_5yR}>U_wOyi@mzNX-#bR15H3&{7e+$ZF5-Jpe0M3g> zqq`mjK0kb-z=(2rMf5l$`u0G90O#?pcRlKEr2C&=C`@=wIrmPiZ`Pp&F4O>sx;OK% z!M1Jb^{8SU#!E>unG{}+s-;JoK_LSlO^();M>zp=+U@p+x7G2!c|pNkIEUNt(u1tr zN$yx$gTcV!WXnJ(e|WEc&<}-&H#tLGVacHT=kVKlp=~kVG)+01&DPm$7Wop0I_@Ww zi8|c4Vs|fjz?@T8B~f_d1)IzuQM~P2+nLt+TBc;JR+}#v+Ey3XLZ;JcH5Vl6+LlQ! zQ>oOV+wHEviMHwTKDZAC{zn5{-#$nf{=R*W48F$;gAP1M8^CVg!-K|OpNQUdq69o@ P00000NkvXXu0mjf?UjO* diff --git a/assets/images/flags/ukr.png b/assets/images/flags/ukr.png index 61071e3381ab6953385f14f217b7d976f84fda8f..c4fe57fcc770d30bce7f6b075313061d81514518 100644 GIT binary patch literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|TRi zr;B4q#hkZS9eY_Ed7LjU5)$w=y>V#q`K6H}%Kc{Vz10$|KUxBHL%;#8RrmS##(#ET zn5#R3A&Oyj{GE-A>lrTT>WEJ;U$kez9!awW-%oOEXLv*vk?;NN1ed5UrjWBZA@!TzbV- z_AZiz3pe=$e;}Wcl?3uN{)D?A=u)#0F~MvEAE5Zand#JNtf8H&xp&zInyT)pZjaS9 zJzYg~5A~?^dOZ(PYptHyJZPS6fB(i@&yrrsHX&vJGTc@zHk)RRa=EO<{&YI2TrQ{OIp$%k-EM2FgK>=6>xw>^OlUft#@%jL_xpXU z5!*yT(7#+RnQS)eU$0jchM_7H3LtHM5PBw)(VNYN{)hA&(cfzq1c7Eff38$25zA;a z8ih(J<6>g={&Kne@U!{-7k6LC4gBu>{qd1yI2a6~fEBpjV=p2;TpuaIK-kmO|(=h-Fs95 z)@2x?60j~KusizO7jg-of4_cueLUePa1pPLr)srIt^v*yj|Sv0;5_2EIjPw3~?owiS!z2TN`4FfBC-e>*MhlHX04& zOCTC22R;*xdGN~azVL(-r>;h#@xo^|IYOe@<2T!*==pr!$l-8!Iy1yp8f+o!^}1OL z5`D2{l9yVoc3vzNS8$?Ry8H-9!602Ukp8V8;iaEFDl(*s7alz+NWkt<;X&{ZY{#Tb UOTz}200000NkvXX1g=70f;o9SYXATM diff --git a/assets/images/nanogpt.png b/assets/images/nanogpt.png new file mode 100644 index 0000000000000000000000000000000000000000..958400452e05570204910444202a6a311c23f26f GIT binary patch literal 516852 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 m6`j&b_Yq+f@femNq5kc@uzz=!MOqlTE047RP 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⋙CODWU4U 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�NKbaI0-wLnD*$7=aAK(34>bd8bn?DbgjHk2@fq@ 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
9iU-@rb*T}j2U9aa@BP7Evo&bg=#*eCf0tP3e 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

wB)B-&LvD@DhilZ+~$EG$=EbRK15 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 D-%FH* literal 0 HcmV?d00001 diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart index 3307bfeed..575a61b0b 100644 --- a/cw_bitcoin/lib/exceptions.dart +++ b/cw_bitcoin/lib/exceptions.dart @@ -30,4 +30,6 @@ class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailed class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {} +class BitcoinTransactionCommitFailedLessThanMin extends TransactionCommitFailedBIP68Final {} + class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {} diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index e90e2193a..aeba9394a 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -91,6 +91,10 @@ class PendingBitcoinTransaction with PendingTransaction { throw BitcoinTransactionCommitFailedBIP68Final(); } + if (error.contains("min fee not met")) { + throw BitcoinTransactionCommitFailedLessThanMin(); + } + throw BitcoinTransactionCommitFailed(errorMessage: error); } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index cf7a73b3c..d3ef6526a 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -178,7 +178,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/dcr_icon.png', decimals: 8); static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png', decimals: 8); static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png', decimals: 18); - static const maticpoly = CryptoCurrency(title: 'MATIC', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18); + static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18); static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/matic_icon.png', decimals: 18); static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png', decimals: 18); static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png', decimals: 24); diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index dccacd799..c13861698 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -36,4 +36,6 @@ class TransactionCommitFailedVoutNegative implements Exception {} class TransactionCommitFailedBIP68Final implements Exception {} +class TransactionCommitFailedLessThanMin implements Exception {} + class TransactionInputNotSupported implements Exception {} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index 51aeab5e1..e8a9e85fa 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -142,7 +142,7 @@ class EthereumWallet extends EVMChainWallet { if (!hasKeysFile) rethrow; } - final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ?? + final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String?) ?? EVMChainERC20Balance(BigInt.zero); final WalletKeysData keysData; diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index c89fb222f..67ee75d7c 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -298,7 +298,9 @@ class MoonPayProvider extends BuyProvider { String _normalizeCurrency(CryptoCurrency currency) { if (currency == CryptoCurrency.maticpoly) { - return "MATIC_POLYGON"; + return "POL_POLYGON"; + } else if (currency == CryptoCurrency.matic) { + return "POL"; } return currency.toString().toLowerCase(); diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 42e55d2b7..e76cb99d1 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -5,9 +5,6 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; -const BEFORE_REGEX = '(^|\s)'; -const AFTER_REGEX = '(\$|\s)'; - class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) : super( @@ -22,23 +19,21 @@ class AddressValidator extends TextValidator { length: getLength(type)); static String getPattern(CryptoCurrency type) { - var pattern = ""; if (type is Erc20Token) { - pattern = '0x[0-9a-zA-Z]'; + return '0x[0-9a-zA-Z]'; } switch (type) { case CryptoCurrency.xmr: - pattern = '4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}'; + return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; case CryptoCurrency.ada: - pattern = '[0-9a-zA-Z]{59}|[0-9a-zA-Z]{92}|[0-9a-zA-Z]{104}' - '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; + return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' + '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; case CryptoCurrency.btc: - pattern = - '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${SilentPaymentAddress.regex.pattern}\$'; case CryptoCurrency.nano: - pattern = '[0-9a-zA-Z_]'; + return '[0-9a-zA-Z_]'; case CryptoCurrency.banano: - pattern = '[0-9a-zA-Z_]'; + return '[0-9a-zA-Z_]'; case CryptoCurrency.usdc: case CryptoCurrency.usdcpoly: case CryptoCurrency.usdtPoly: @@ -74,11 +69,11 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dydx: case CryptoCurrency.steth: case CryptoCurrency.shib: - pattern = '0x[0-9a-zA-Z]'; + return '0x[0-9a-zA-Z]'; case CryptoCurrency.xrp: - pattern = '[0-9a-zA-Z]{34}|X[0-9a-zA-Z]{46}'; + return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; case CryptoCurrency.xhv: - pattern = 'hvx|hvi|hvs[0-9a-zA-Z]'; + return '^hvx|hvi|hvs[0-9a-zA-Z]'; case CryptoCurrency.xag: case CryptoCurrency.xau: case CryptoCurrency.xaud: @@ -100,43 +95,40 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dash: case CryptoCurrency.eos: case CryptoCurrency.wow: - pattern = '[0-9a-zA-Z]'; + return '[0-9a-zA-Z]'; case CryptoCurrency.bch: - pattern = - '(?!bitcoincash:)[0-9a-zA-Z]*|(?!bitcoincash:)q|p[0-9a-zA-Z]{41}|(?!bitcoincash:)q|p[0-9a-zA-Z]{42}|bitcoincash:q|p[0-9a-zA-Z]{41}|bitcoincash:q|p[0-9a-zA-Z]{42}'; + return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{42}\$|^bitcoincash:q|p[0-9a-zA-Z]{41}\$|^bitcoincash:q|p[0-9a-zA-Z]{42}\$'; case CryptoCurrency.bnb: - pattern = '[0-9a-zA-Z]'; + return '[0-9a-zA-Z]'; case CryptoCurrency.ltc: - pattern = '(?!(ltc|LTC)1)[0-9a-zA-Z]*|(LTC1[A-Z0-9]*)|(ltc1[a-z0-9]*)'; + return '^(?!(ltc|LTC)1)[0-9a-zA-Z]*\$|(^LTC1[A-Z0-9]*\$)|(^ltc1[a-z0-9]*\$)'; case CryptoCurrency.hbar: - pattern = '[0-9a-zA-Z.]'; + return '[0-9a-zA-Z.]'; case CryptoCurrency.zaddr: - pattern = 'zs[0-9a-zA-Z]{75}'; + return '^zs[0-9a-zA-Z]{75}'; case CryptoCurrency.zec: - pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; + return '^t1[0-9a-zA-Z]{33}\$|^t3[0-9a-zA-Z]{33}\$'; case CryptoCurrency.dcr: - pattern = 'D[ksecS]([0-9a-zA-Z])+'; + return 'D[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: - pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; + return '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.near: - pattern = '[0-9a-f]{64}'; + return '[0-9a-f]{64}'; case CryptoCurrency.rune: - pattern = 'thor1[0-9a-z]{38}'; + return 'thor1[0-9a-z]{38}'; case CryptoCurrency.scrt: - pattern = 'secret1[0-9a-z]{38}'; + return 'secret1[0-9a-z]{38}'; case CryptoCurrency.stx: - pattern = 'S[MP][0-9a-zA-Z]+'; + return 'S[MP][0-9a-zA-Z]+'; case CryptoCurrency.kmd: - pattern = 'R[0-9a-zA-Z]{33}'; + return 'R[0-9a-zA-Z]{33}'; case CryptoCurrency.pivx: - pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}'; + return 'D([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.btcln: - pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; + return '^(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; default: - pattern = '[0-9a-zA-Z]'; + return '[0-9a-zA-Z]'; } - - return '$BEFORE_REGEX$pattern$AFTER_REGEX'; } static List? getLength(CryptoCurrency type) { @@ -277,54 +269,56 @@ class AddressValidator extends TextValidator { } static String? getAddressFromStringPattern(CryptoCurrency type) { - String? pattern = null; - switch (type) { case CryptoCurrency.xmr: case CryptoCurrency.wow: - pattern = '4[0-9a-zA-Z]{94}' - '|8[0-9a-zA-Z]{94}' - '|[0-9a-zA-Z]{106}'; + return '([^0-9a-zA-Z]|^)4[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; case CryptoCurrency.btc: - pattern = - '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type + '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)' //P2trAddress type + '|${SilentPaymentAddress.regex.pattern}\$'; + case CryptoCurrency.ltc: - pattern = '^L[a-zA-Z0-9]{26,33}' - '|[LM][a-km-zA-HJ-NP-Z1-9]{26,33}' - '|ltc[a-zA-Z0-9]{26,45}'; + return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; case CryptoCurrency.eth: - pattern = '0x[0-9a-zA-Z]{42}'; + return '0x[0-9a-zA-Z]{42}'; case CryptoCurrency.maticpoly: - pattern = '0x[0-9a-zA-Z]{42}'; + return '0x[0-9a-zA-Z]{42}'; case CryptoCurrency.nano: - pattern = 'nano_[0-9a-zA-Z]{60}'; + return 'nano_[0-9a-zA-Z]{60}'; case CryptoCurrency.banano: - pattern = 'ban_[0-9a-zA-Z]{60}'; + return 'ban_[0-9a-zA-Z]{60}'; case CryptoCurrency.bch: - pattern = '(bitcoincash:)?q[0-9a-zA-Z]{41,42}'; + return 'bitcoincash:q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' + '|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'; case CryptoCurrency.sol: - pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}'; + return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; case CryptoCurrency.trx: - pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; default: if (type.tag == CryptoCurrency.eth.title) { - pattern = '0x[0-9a-zA-Z]{42}'; + return '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.maticpoly.tag) { - pattern = '0x[0-9a-zA-Z]{42}'; + return '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.sol.title) { - pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}'; + return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; } if (type.tag == CryptoCurrency.trx.title) { - pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; } - } - if (pattern != null) { - return "$BEFORE_REGEX$pattern$AFTER_REGEX"; + return null; } - - return null; } } diff --git a/lib/entities/pin_code_required_duration.dart b/lib/entities/pin_code_required_duration.dart index fef5715b5..0c0d6d132 100644 --- a/lib/entities/pin_code_required_duration.dart +++ b/lib/entities/pin_code_required_duration.dart @@ -1,32 +1,39 @@ import 'package:cake_wallet/generated/i18n.dart'; -enum PinCodeRequiredDuration { - always(0), - tenminutes(10), - onehour(60); +enum PinCodeRequiredDuration { + always(0), + tenMinutes(10), + halfHour(30), + fortyFiveMinutes(45), + oneHour(60); const PinCodeRequiredDuration(this.value); + final int value; static PinCodeRequiredDuration deserialize({required int raw}) => PinCodeRequiredDuration.values.firstWhere((e) => e.value == raw); @override - String toString(){ + String toString() { String label = ''; switch (this) { case PinCodeRequiredDuration.always: label = S.current.always; break; - case PinCodeRequiredDuration.tenminutes: + case PinCodeRequiredDuration.tenMinutes: label = S.current.minutes_to_pin_code('10'); break; - case PinCodeRequiredDuration.onehour: + case PinCodeRequiredDuration.oneHour: label = S.current.minutes_to_pin_code('60'); break; + case PinCodeRequiredDuration.halfHour: + label = S.current.minutes_to_pin_code('30'); + break; + case PinCodeRequiredDuration.fortyFiveMinutes: + label = S.current.minutes_to_pin_code('45'); + break; } return label; - } - -} \ No newline at end of file +} diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart index 885878684..be52b73fe 100644 --- a/lib/exchange/provider/simpleswap_exchange_provider.dart +++ b/lib/exchange/provider/simpleswap_exchange_provider.dart @@ -236,7 +236,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { case CryptoCurrency.usdcsol: return 'usdcspl'; case CryptoCurrency.matic: - return 'maticerc20'; + return 'pol'; case CryptoCurrency.maticpoly: return 'matic'; default: diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index b76626684..307107dd4 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -156,7 +156,8 @@ class CWPolygon extends Polygon { @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { transaction as EVMChainTransactionInfo; - if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title) { + if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title || + transaction.tokenSymbol == "MATIC") { return CryptoCurrency.maticpoly; } diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index 0c953c892..37bc3a55f 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -52,20 +52,26 @@ class CakeFeaturesPage extends StatelessWidget { onTap: () => _navigatorToGiftCardsPage(context), title: 'Cake Pay', subTitle: S.of(context).cake_pay_subtitle, - svgPicture: SvgPicture.asset( - 'assets/images/cards.svg', - height: 125, - width: 125, + image: Image.asset( + 'assets/images/cards.png', + height: 100, + width: 115, fit: BoxFit.cover, ), ), SizedBox(height: 10), DashBoardRoundedCardWidget( + onTap: () => _launchUrl("cake.nano-gpt.com"), title: "NanoGPT", subTitle: S.of(context).nanogpt_subtitle, - onTap: () => _launchUrl("cake.nano-gpt.com"), + image: Image.asset( + 'assets/images/nanogpt.png', + height: 80, + width: 80, + fit: BoxFit.cover, + ), ), - SizedBox(height: 20), + SizedBox(height: 10), Observer( builder: (context) { if (!dashboardViewModel.hasSignMessages) { @@ -75,6 +81,12 @@ class CakeFeaturesPage extends StatelessWidget { onTap: () => Navigator.of(context).pushNamed(Routes.signPage), title: S.current.sign_verify_message, subTitle: S.current.sign_verify_message_sub, + icon: Icon( + Icons.speaker_notes_rounded, + color: + Theme.of(context).extension()!.pageTitleTextColor, + size: 75, + ), ); }, ), diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index cb5086fe1..472f311c9 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -8,7 +8,6 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/option_tile.dart'; -import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -20,6 +19,9 @@ import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/info_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; class RestoreOptionsPage extends BasePage { RestoreOptionsPage({required this.isNewInstall}); @@ -44,12 +46,16 @@ class RestoreOptionsPage extends BasePage { @override Widget body(BuildContext context) { - final imageColor = Theme.of(context).extension()!.titleColor; + final mainImageColor = Theme.of(context).extension()!.pageTitleTextColor; + final brightImageColor = Theme.of(context).extension()!.textColor; + final imageColor = currentTheme.type == ThemeType.bright ? brightImageColor : mainImageColor; final imageLedger = Image.asset('assets/images/ledger_nano.png', width: 40, color: imageColor); final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor); final imageBackup = Image.asset('assets/images/backup.png', color: imageColor); final qrCode = Image.asset('assets/images/restore_qr.png', color: imageColor); + + return Center( child: Container( width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, @@ -96,12 +102,15 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - key: ValueKey('restore_options_from_qr_button_key'), - onPressed: () => _onScanQRCode(context), - image: qrCode, - title: S.of(context).scan_qr_code, - description: S.of(context).cold_or_recover_wallet, - ), + key: ValueKey('restore_options_from_qr_button_key'), + onPressed: () => _onScanQRCode(context), + icon: Icon( + Icons.qr_code_rounded, + color: imageColor, + size: 50, + ), + title: S.of(context).scan_qr_code, + description: S.of(context).cold_or_recover_wallet), ) ], ), diff --git a/lib/src/screens/support/support_page.dart b/lib/src/screens/support/support_page.dart index 471ff15b0..049ab2fb7 100644 --- a/lib/src/screens/support/support_page.dart +++ b/lib/src/screens/support/support_page.dart @@ -2,17 +2,20 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/option_tile.dart'; +import 'package:cake_wallet/themes/extensions/info_theme.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; class SupportPage extends BasePage { SupportPage(this.supportViewModel); final SupportViewModel supportViewModel; - final imageLiveSupport = Image.asset('assets/images/live_support.png'); + final imageLiveSupport = Image.asset('assets/images/cake_icon.png'); final imageWalletGuides = Image.asset('assets/images/wallet_guides.png'); final imageMoreLinks = Image.asset('assets/images/more_links.png'); @@ -22,8 +25,16 @@ class SupportPage extends BasePage { @override AppBarStyle get appBarStyle => AppBarStyle.regular; + + @override Widget body(BuildContext context) { + + final mainColor = Theme.of(context).extension()!.pageTitleTextColor; + final brightColor = Theme.of(context).extension()!.textColor; + + final iconColor = currentTheme.type == ThemeType.bright ? brightColor : mainColor; + return Container( child: Center( child: ConstrainedBox( @@ -33,7 +44,11 @@ class SupportPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - image: imageLiveSupport, + icon: Icon( + Icons.support_agent, + color: iconColor, + size: 50, + ), title: S.of(context).support_title_live_chat, description: S.of(context).support_description_live_chat, onPressed: () { @@ -48,7 +63,11 @@ class SupportPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - image: imageWalletGuides, + icon: Icon( + Icons.find_in_page, + color: iconColor, + size: 50, + ), title: S.of(context).support_title_guides, description: S.of(context).support_description_guides, onPressed: () => _launchUrl(supportViewModel.guidesUrl), @@ -57,7 +76,11 @@ class SupportPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - image: imageMoreLinks, + icon: Icon( + Icons.contact_support, + color: iconColor, + size: 50, + ), title: S.of(context).support_title_other_links, description: S.of(context).support_description_other_links, onPressed: () => Navigator.pushNamed(context, Routes.supportOtherLinks), diff --git a/lib/src/screens/support_other_links/support_other_links_page.dart b/lib/src/screens/support_other_links/support_other_links_page.dart index 7a1a945ca..b115594b4 100644 --- a/lib/src/screens/support_other_links/support_other_links_page.dart +++ b/lib/src/screens/support_other_links/support_other_links_page.dart @@ -37,7 +37,6 @@ class SupportOtherLinksPage extends BasePage { itemCounter: (int _) => supportViewModel.items.length, itemBuilder: (_, index) { final item = supportViewModel.items[index]; - if (item is RegularListItem) { return SettingsCellWithArrow(title: item.title, handler: item.handler); } diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index 5a8ca14a4..4013dd79e 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -11,6 +11,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { required this.subTitle, this.hint, this.svgPicture, + this.image, this.icon, this.onClose, this.customBorder, @@ -22,6 +23,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { final String subTitle; final Widget? hint; final SvgPicture? svgPicture; + final Image? image; final Icon? icon; final double? customBorder; @@ -77,7 +79,8 @@ class DashBoardRoundedCardWidget extends StatelessWidget { ], ), ), - if (svgPicture != null) svgPicture!, + if (image != null) image! + else if (svgPicture != null) svgPicture!, if (icon != null) icon! ], ), diff --git a/lib/src/widgets/option_tile.dart b/lib/src/widgets/option_tile.dart index f7811a888..31f958f54 100644 --- a/lib/src/widgets/option_tile.dart +++ b/lib/src/widgets/option_tile.dart @@ -4,13 +4,16 @@ import 'package:flutter/material.dart'; class OptionTile extends StatelessWidget { const OptionTile( {required this.onPressed, - required this.image, + this.image, + this.icon, required this.title, required this.description, - super.key}); + super.key}) + : assert(image!=null || icon!=null); final VoidCallback onPressed; - final Image image; + final Image? image; + final Icon? icon; final String title; final String description; @@ -31,7 +34,7 @@ class OptionTile extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - image, + icon ?? image!, Expanded( child: Padding( padding: EdgeInsets.only(left: 16), diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 6873ffde8..debaeb07a 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -570,7 +570,7 @@ abstract class SettingsStoreBase with Store { static const defaultPinLength = 4; static const defaultActionsMode = 11; - static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes; + static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenMinutes; static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; static final walletPasswordDirectInput = Platform.isLinux; static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index dd58d3e56..e3a571eea 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -21,9 +21,9 @@ class ExceptionHandler { static File? _file; static void _saveException(String? error, StackTrace? stackTrace, {String? library}) async { - if (_file == null) { - final appDocDir = await getAppDir(); + final appDocDir = await getAppDir(); + if (_file == null) { _file = File('${appDocDir.path}/error.txt'); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index e8b00eb7e..c1ab9d822 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -692,6 +692,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (error is TransactionCommitFailedBIP68Final) { return S.current.tx_rejected_bip68_final; } + if (error is TransactionCommitFailedLessThanMin) { + return S.current.fee_less_than_min; + } if (error is TransactionNoDustOnChangeException) { return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); } diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index 2bb749b42..45b9cb9bf 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -14,14 +14,16 @@ abstract class SupportViewModelBase with Store { : items = [ LinkListItem( title: 'Email', + icon: 'assets/images/support_icon.png', linkTitle: 'support@cakewallet.com', link: 'mailto:support@cakewallet.com'), if (!isMoneroOnly) LinkListItem( title: 'Website', + icon: 'assets/images/global.png', linkTitle: 'cakewallet.com', link: 'https://cakewallet.com'), - if (!isMoneroOnly) + if (!isMoneroOnly) LinkListItem( title: 'GitHub', icon: 'assets/images/github.png', @@ -29,7 +31,12 @@ abstract class SupportViewModelBase with Store { linkTitle: S.current.apk_update, link: 'https://github.com/cake-tech/cake_wallet/releases'), LinkListItem( - title: 'Telegram', + title: 'Telegram Community', + icon: 'assets/images/Telegram.png', + linkTitle: 't.me/cakewallet', + link: 'https://t.me/cakewallet'), + LinkListItem( + title: 'Telegram Support Bot', icon: 'assets/images/Telegram.png', linkTitle: '@cakewallet_bot', link: 'https://t.me/cakewallet_bot'), diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 20ad96eef..12ecceed9 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -129,6 +129,7 @@ "choose_from_available_options": "اختر من بين الخيارات المتاحة:", "choose_one": "اختر واحدة", "choose_relay": "ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ", + "choose_wallet_currency": "الرجاء اختيار عملة المحفظة:", "choose_wallet_group": "اختر مجموعة المحفظة", "clear": "مسح", "clearnet_link": "رابط Clearnet", @@ -297,6 +298,7 @@ "failed_authentication": "${state_error} فشل المصادقة.", "faq": "الأسئلة الشائعة", "features": "سمات", + "fee_less_than_min": "الرسوم المحددة أقل من الحد الأدنى ، يرجى زيادة الرسوم لتكون قادرة على إرسال المعاملة", "fee_rate": "معدل الرسوم", "fetching": "جار الجلب", "fiat_api": "Fiat API", @@ -605,6 +607,7 @@ "seedtype_alert_title": "تنبيه البذور", "seedtype_legacy": "إرث (25 كلمة)", "seedtype_polyseed": "بوليسيد (16 كلمة)", + "seedtype_wownero": "Wownero (14 كلمة)", "select_backup_file": "حدد ملف النسخ الاحتياطي", "select_buy_provider_notice": "حدد مزود شراء أعلاه. يمكنك تخطي هذه الشاشة عن طريق تعيين مزود شراء الافتراضي في إعدادات التطبيق.", "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", @@ -676,7 +679,7 @@ "show_seed": "عرض السييد", "sign_message": "تسجيل رسالة", "sign_up": "اشتراك", - "sign_verify_message": "توقيع أو التحقق من الرسالة", + "sign_verify_message": "تسجيل / تحقق", "sign_verify_message_sub": "قم بتوقيع أو التحقق من رسالة باستخدام المفتاح الخاص بك", "sign_verify_title": "تسجيل / تحقق", "signature": "إمضاء", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index d07ed0509..b1a1096e6 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -298,6 +298,7 @@ "failed_authentication": "Неуспешно удостоверяване. ${state_error}", "faq": "FAQ", "features": "Характеристика", + "fee_less_than_min": "Избраната такса е по -малка от минимума, моля, увеличете таксите, за да можете да изпратите транзакцията", "fee_rate": "Такса ставка", "fetching": "Обработване", "fiat_api": "Fiat API", @@ -606,6 +607,7 @@ "seedtype_alert_title": "Сигнал за семена", "seedtype_legacy": "Наследство (25 думи)", "seedtype_polyseed": "Поли семе (16 думи)", + "seedtype_wownero": "Wownero (14 думи)", "select_backup_file": "Избор на резервно копие", "select_buy_provider_notice": "Изберете доставчик на покупка по -горе. Можете да пропуснете този екран, като зададете вашия доставчик по подразбиране по подразбиране в настройките на приложението.", "select_destination": "Моля, изберете дестинация за архивния файл.", @@ -677,7 +679,7 @@ "show_seed": "Покажи seed", "sign_message": "Съобщение за подписване", "sign_up": "Регистрация", - "sign_verify_message": "Подпишете или проверете съобщението", + "sign_verify_message": "Подпишете / проверете", "sign_verify_message_sub": "Подпишете или проверете съобщение с помощта на вашия личен ключ", "sign_verify_title": "Подпишете / проверете", "signature": "Подпис", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 40ca8fb2c..7ce797845 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -298,6 +298,7 @@ "failed_authentication": "Ověřování selhalo. ${state_error}", "faq": "FAQ", "features": "Funkce", + "fee_less_than_min": "Vybraný poplatek je menší než minimum, zvýšit poplatky, abyste mohli transakci odeslat", "fee_rate": "Sazba poplatků", "fetching": "Načítá se", "fiat_api": "Fiat API", @@ -606,6 +607,7 @@ "seedtype_alert_title": "Upozornění seedtype", "seedtype_legacy": "Legacy (25 slov)", "seedtype_polyseed": "Polyseed (16 slov)", + "seedtype_wownero": "Wownero (14 slov)", "select_backup_file": "Vybrat soubor se zálohou", "select_buy_provider_notice": "Vyberte výše uvedeného poskytovatele nákupu. Tuto obrazovku můžete přeskočit nastavením výchozího poskytovatele nákupu v nastavení aplikace.", "select_destination": "Vyberte cíl pro záložní soubor.", @@ -677,7 +679,7 @@ "show_seed": "Zobrazit seed", "sign_message": "Podepsat zprávu", "sign_up": "Registrovat se", - "sign_verify_message": "Podepište nebo ověřte zprávu", + "sign_verify_message": "Podepsat / ověřit", "sign_verify_message_sub": "Podepište nebo ověřte zprávu pomocí soukromého klíče", "sign_verify_title": "Podepsat / ověřit", "signature": "Podpis", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 3c7e9fbc9..e599da769 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -298,6 +298,7 @@ "failed_authentication": "Authentifizierung fehlgeschlagen. ${state_error}", "faq": "Häufig gestellte Fragen", "features": "Merkmale", + "fee_less_than_min": "Die ausgewählte Gebühr ist geringer als das Minimum. Bitte erhöhen Sie die Gebühren, um die Transaktion senden zu können", "fee_rate": "Gebührenpreis", "fetching": "Frage ab", "fiat_api": "Fiat API", @@ -607,6 +608,7 @@ "seedtype_alert_title": "Seedype -Alarm", "seedtype_legacy": "Veraltet (25 Wörter)", "seedtype_polyseed": "Polyseed (16 Wörter)", + "seedtype_wownero": "WOWNO (14 Wörter)", "select_backup_file": "Sicherungsdatei auswählen", "select_buy_provider_notice": "Wählen Sie oben einen Anbieter kaufen. Sie können diese Seite überspringen, indem Sie Ihren Standard-Kaufanbieter in den App-Einstellungen festlegen.", "select_destination": "Bitte wählen Sie das Ziel für die Sicherungsdatei aus.", @@ -678,7 +680,7 @@ "show_seed": "Seed zeigen", "sign_message": "Nachricht unterschreiben", "sign_up": "Anmelden", - "sign_verify_message": "Nachricht unterschreiben oder überprüfen", + "sign_verify_message": "Zeichen / überprüfen", "sign_verify_message_sub": "Unterschreiben oder überprüfen Sie eine Nachricht mit Ihrem privaten Schlüssel", "sign_verify_title": "Zeichen / überprüfen", "signature": "Signatur", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 7b98ed3d6..72e05b8b5 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -299,6 +299,7 @@ "faq": "FAQ", "features": "Features", "fee_rate": "Fee rate", + "fee_less_than_min": "Selected Fee is less than the minimum, please increase the fees to be able to send the transaction", "fetching": "Fetching", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -678,7 +679,7 @@ "show_seed": "Show seed", "sign_message": "Sign Message", "sign_up": "Sign Up", - "sign_verify_message": "Sign or verify message", + "sign_verify_message": "Sign / Verify", "sign_verify_message_sub": "Sign or verify a message using your private key", "sign_verify_title": "Sign / Verify", "signature": "Signature", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 4043e5ac3..2cdc2318a 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -298,6 +298,7 @@ "failed_authentication": "Autenticación fallida. ${state_error}", "faq": "FAQ", "features": "Características", + "fee_less_than_min": "La tarifa seleccionada es menor que la mínima, aumente las tarifas para poder enviar la transacción", "fee_rate": "Tarifa", "fetching": "Cargando", "fiat_api": "Fiat API", @@ -607,6 +608,7 @@ "seedtype_alert_title": "Alerta de type de semillas", "seedtype_legacy": "Legado (25 palabras)", "seedtype_polyseed": "Polieta (16 palabras)", + "seedtype_wownero": "Wownero (14 palabras)", "select_backup_file": "Seleccionar archivo de respaldo", "select_buy_provider_notice": "Seleccione un proveedor de compra arriba. Puede omitir esta pantalla configurando su proveedor de compra predeterminado en la configuración de la aplicación.", "select_destination": "Seleccione el destino del archivo de copia de seguridad.", @@ -678,7 +680,7 @@ "show_seed": "Mostrar semilla", "sign_message": "Mensaje de firma", "sign_up": "Registrarse", - "sign_verify_message": "Firmar o verificar el mensaje", + "sign_verify_message": "Firmar / verificar", "sign_verify_message_sub": "Firmar o verificar un mensaje usando su clave privada", "sign_verify_title": "Firmar / verificar", "signature": "Firma", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 7bbc1076b..27109c0ae 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -298,6 +298,7 @@ "failed_authentication": "Échec d'authentification. ${state_error}", "faq": "FAQ", "features": "Caractéristiques", + "fee_less_than_min": "Les frais sélectionnés sont inférieurs au minimum, veuillez augmenter les frais pour pouvoir envoyer la transaction", "fee_rate": "Taux de frais", "fetching": "Récupération", "fiat_api": "Fiat API", @@ -606,6 +607,7 @@ "seedtype_alert_title": "Alerte de type SeedType", "seedtype_legacy": "Héritage (25 mots)", "seedtype_polyseed": "Polyseed (16 mots)", + "seedtype_wownero": "WOWNERO (14 mots)", "select_backup_file": "Sélectionnez le fichier de sauvegarde", "select_buy_provider_notice": "Sélectionnez un fournisseur d'achat ci-dessus. Vous pouvez ignorer cet écran en définissant votre fournisseur d'achat par défaut dans les paramètres de l'application.", "select_destination": "Veuillez sélectionner la destination du fichier de sauvegarde.", @@ -677,7 +679,7 @@ "show_seed": "Visualiser la phrase secrète (seed)", "sign_message": "Signer le message", "sign_up": "S'inscrire", - "sign_verify_message": "Signer ou vérifier le message", + "sign_verify_message": "Signe / vérifier", "sign_verify_message_sub": "Signez ou vérifiez un message en utilisant votre clé privée", "sign_verify_title": "Signe / vérifier", "signature": "Signature", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 6f551a656..1c293bd54 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -298,6 +298,7 @@ "failed_authentication": "Binne wajen shiga. ${state_error}", "faq": "FAQ", "features": "Fasas", + "fee_less_than_min": "Kudin da aka zaba kasa da mafi karancin, don Allah ƙara kudaden don samun damar aika ma'amala", "fee_rate": "Kudi", "fetching": "Daukewa", "fiat_api": "API ɗin Fiat", @@ -608,6 +609,7 @@ "seedtype_alert_title": "Seedtype farke", "seedtype_legacy": "Legacy (25 kalmomi)", "seedtype_polyseed": "Polyseed (16 kalmomi)", + "seedtype_wownero": "WowRero (kalmomi 14)", "select_backup_file": "Zaɓi fayil ɗin madadin", "select_buy_provider_notice": "Zaɓi mai ba da kyauta a sama. Zaka iya tsallake wannan allon ta hanyar saita mai ba da isasshen busasshen mai ba da isasshen busasshiyar saiti.", "select_destination": "Da fatan za a zaɓi wurin da za a yi wa madadin fayil ɗin.", @@ -679,7 +681,7 @@ "show_seed": "Nuna iri", "sign_message": "Sa hannu", "sign_up": "Shiga", - "sign_verify_message": "Shiga ko Tabbatar da Saƙo", + "sign_verify_message": "Sa hannu / Tabbatar", "sign_verify_message_sub": "Shiga ko tabbatar da saƙo ta amfani da Maɓallinku na sirri", "sign_verify_title": "Sa hannu / Tabbatar", "signature": "Sa hannu", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 8e3990552..a93e20ea0 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -236,7 +236,7 @@ "email_address": "ईमेल पता", "enable_mempool_api": "सटीक शुल्क और तिथियों के लिए मेमपूल एपीआई", "enable_replace_by_fee": "प्रतिस्थापित-दर-शुल्क सक्षम करें", - "enable_silent_payments_scanning": "साइलेंट पेमेंट्स को स्कैन करना शुरू करें, जब तक कि टिप तक पहुंच न जाए", + "enable_silent_payments_scanning": "मूक भुगतान स्कैनिंग सक्षम करें", "enabled": "सक्रिय", "enter_amount": "राशि दर्ज करें", "enter_backup_password": "यहां बैकअप पासवर्ड डालें", @@ -298,6 +298,7 @@ "failed_authentication": "प्रमाणीकरण विफल. ${state_error}", "faq": "FAQ", "features": "विशेषताएँ", + "fee_less_than_min": "चयनित शुल्क न्यूनतम से कम है, कृपया लेन -देन भेजने में सक्षम होने के लिए शुल्क बढ़ाएं", "fee_rate": "शुल्क दर", "fetching": "ला रहा है", "fiat_api": "फिएट पैसे API", @@ -604,10 +605,11 @@ "seed_share": "बीज साझा करें", "seed_title": "बीज", "seedtype": "बीज", - "seedtype_alert_content": "अन्य बटुए के साथ बीज साझा करना केवल BIP39 SEEDTYPE के साथ संभव है।", + "seedtype_alert_content": "अन्य पर्स के साथ बीज साझा करना केवल BIP39 सीडटाइप के साथ संभव है।", "seedtype_alert_title": "बीजगणित अलर्ट", "seedtype_legacy": "विरासत (25 शब्द)", "seedtype_polyseed": "पॉलीसीड (16 शब्द)", + "seedtype_wownero": "Wownero (14 शब्द)", "select_backup_file": "बैकअप फ़ाइल का चयन करें", "select_buy_provider_notice": "ऊपर एक खरीद प्रदाता का चयन करें। आप इस स्क्रीन को ऐप सेटिंग्स में अपना डिफ़ॉल्ट बाय प्रदाता सेट करके छोड़ सकते हैं।", "select_destination": "कृपया बैकअप फ़ाइल के लिए गंतव्य का चयन करें।", @@ -679,7 +681,7 @@ "show_seed": "बीज दिखाओ", "sign_message": "हस्ताक्षर संदेश", "sign_up": "साइन अप करें", - "sign_verify_message": "संदेश पर हस्ताक्षर या सत्यापित करें", + "sign_verify_message": "हस्ताक्षर / सत्यापित करें", "sign_verify_message_sub": "अपनी निजी कुंजी का उपयोग करके किसी संदेश पर हस्ताक्षर या सत्यापित करें", "sign_verify_title": "हस्ताक्षर / सत्यापित करें", "signature": "हस्ताक्षर", @@ -861,8 +863,8 @@ "wallet_group_description_four": "एक पूरी तरह से नए बीज के साथ एक बटुआ बनाने के लिए।", "wallet_group_description_one": "केक बटुए में, आप एक बना सकते हैं", "wallet_group_description_three": "उपलब्ध वॉलेट और/या वॉलेट समूह स्क्रीन देखने के लिए। या चुनें", - "wallet_group_description_two": "एक बीज साझा करने के लिए एक मौजूदा बटुए का चयन करके। प्रत्येक वॉलेट समूह में प्रत्येक मुद्रा प्रकार का एक एकल वॉलेट हो सकता है। \n\n आप चयन कर सकते हैं", - "wallet_group_empty_state_text_one": "लगता है कि आपके पास कोई संगत बटुआ समूह नहीं है !\n\n टैप करें", + "wallet_group_description_two": "एक बीज साझा करने के लिए एक मौजूदा बटुए का चयन करके। प्रत्येक वॉलेट समूह में प्रत्येक मुद्रा प्रकार का एक एकल वॉलेट हो सकता है।\n\nआप चयन कर सकते हैं", + "wallet_group_empty_state_text_one": "लगता है कि आपके पास कोई संगत बटुआ समूह नहीं है!\n\nनल", "wallet_group_empty_state_text_two": "नीचे एक नया बनाने के लिए।", "wallet_keys": "बटुआ बीज / चाबियाँ", "wallet_list_create_new_wallet": "नया बटुआ बनाएँ", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 648f996ec..b60a056fd 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -298,6 +298,7 @@ "failed_authentication": "Autentifikacija neuspješna. ${state_error}", "faq": "FAQ", "features": "Značajke", + "fee_less_than_min": "Odabrana naknada je manja od minimalnog, povećajte naknade kako biste mogli poslati transakciju", "fee_rate": "Stopa naknade", "fetching": "Dohvaćanje", "fiat_api": "Fiat API", @@ -606,6 +607,7 @@ "seedtype_alert_title": "Upozorenje o sjemenu", "seedtype_legacy": "Nasljeđe (25 riječi)", "seedtype_polyseed": "Poliseed (16 riječi)", + "seedtype_wownero": "WANERO (14 riječi)", "select_backup_file": "Odaberite datoteku sigurnosne kopije", "select_buy_provider_notice": "Odaberite gornji davatelj kupnje. Ovaj zaslon možete preskočiti postavljanjem zadanog davatelja usluga kupnje u postavkama aplikacija.", "select_destination": "Odaberite odredište za datoteku sigurnosne kopije.", @@ -677,7 +679,7 @@ "show_seed": "Prikaži pristupni izraz", "sign_message": "Poruka", "sign_up": "Prijavite se", - "sign_verify_message": "Potpisati ili provjeriti poruku", + "sign_verify_message": "Potpisati / provjeriti", "sign_verify_message_sub": "Potpišite ili provjerite poruku pomoću privatnog ključa", "sign_verify_title": "Potpisati / provjeriti", "signature": "Potpis", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 786b4cfee..eeb1d3f99 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -198,6 +198,7 @@ "delete_wallet_confirm_message": "Դուք վստահ եք, որ ուզում եք ջնջել ${wallet_name} դրամապանակը?", "deleteConnectionConfirmationPrompt": "Դուք վստահ եք, որ ուզում եք ջնջել կապը", "denominations": "Անվանակարգեր", + "derivationpath": "Ածանցման ուղին", "descending": "Նվազող", "description": "Նկարագրություն", "destination_tag": "Նպատակակետի պիտակ:", @@ -235,7 +236,7 @@ "email_address": "Էլ. փոստի հասցե", "enable_mempool_api": "Mempool API ճշգրիտ վճարների եւ ամսաթվերի համար", "enable_replace_by_fee": "Միացնել փոխարինումը միջնորդավճարով", - "enable_silent_payments_scanning": "Սկսեք սկանավորել լուռ վճարումները, մինչեւ որ ծայրը հասնի", + "enable_silent_payments_scanning": "Միացնել Լուռ Վճարումների սկանավորումը", "enabled": "Միացված", "enter_amount": "Մուտքագրեք գումար", "enter_backup_password": "Մուտքագրեք կրկնօրինակի գաղտնաբառը", @@ -297,6 +298,7 @@ "failed_authentication": "Վավերացումը ձախողվեց. ${state_error}", "faq": "Հաճախ տրվող հարցեր", "features": "Հատկանիշներ", + "fee_less_than_min": "Ընտրված վճարը նվազագույնից պակաս է, խնդրում ենք ավելացնել վճարները, որպեսզի կարողանաք գործարքը ուղարկել", "fee_rate": "Վճարման տոկոսադրույքը", "fetching": "Ստացվում է", "fiat_api": "Fiat API", @@ -677,7 +679,7 @@ "show_seed": "Ցուցադրել սերմ", "sign_message": "Կնքել հաղորդագրություն", "sign_up": "Գրանցվել", - "sign_verify_message": "Կնքել կամ ստուգել հաղորդագրություն", + "sign_verify_message": "Նշեք / ստուգեք", "sign_verify_message_sub": "Կնքել կամ ստուգել հաղորդագրությունը ձեր գախտնի բանալիով", "sign_verify_title": "Կնքել / Ստուգել", "signature": "Կնիք", @@ -859,8 +861,8 @@ "wallet_group_description_four": "Ամբողջովին նոր սերմով դրամապանակ ստեղծելու համար:", "wallet_group_description_one": "Տորթի դրամապանակում կարող եք ստեղծել ա", "wallet_group_description_three": "Տեսնել առկա դրամապանակներն ու (կամ) դրամապանակների խմբերի էկրանը: Կամ ընտրել", - "wallet_group_description_two": "ընտրելով գոյություն ունեցող դրամապանակ `սերմը կիսելու համար: Դրամապանակների յուրաքանչյուր խումբ կարող է պարունակել յուրաքանչյուր արժույթի տիպի մեկ դրամապանակ: \n\n Կարող եք ընտրել", - "wallet_group_empty_state_text_one": "Կարծես թե դուք չունեք որեւէ համատեղելի դրամապանակների խմբեր !\n\n թակել", + "wallet_group_description_two": "ընտրելով գոյություն ունեցող դրամապանակ `սերմը կիսելու համար: Դրամապանակների յուրաքանչյուր խումբ կարող է պարունակել յուրաքանչյուր արժույթի տեսակի մեկ դրամապանակ:\n\nԿարող եք ընտրել", + "wallet_group_empty_state_text_one": "Կարծես թե որեւէ համատեղելի դրամապանակի խմբեր չունեք:\n\nԹակել", "wallet_group_empty_state_text_two": "ներքեւում `նորը կազմելու համար:", "wallet_keys": "Դրամապանակի սերմ/բանալիներ", "wallet_list_create_new_wallet": "Ստեղծել Նոր Դրամապանակ", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 530f78eef..5e48ff1d3 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -298,6 +298,7 @@ "failed_authentication": "Otentikasi gagal. ${state_error}", "faq": "Pertanyaan yang Sering Diajukan", "features": "Fitur", + "fee_less_than_min": "Biaya yang dipilih kurang dari minimum, harap tingkatkan biaya untuk dapat mengirim transaksi", "fee_rate": "Tarif biaya", "fetching": "Mengambil", "fiat_api": "API fiat", @@ -609,6 +610,7 @@ "seedtype_alert_title": "Peringatan seedtype", "seedtype_legacy": "Legacy (25 kata)", "seedtype_polyseed": "Polyseed (16 kata)", + "seedtype_wownero": "Wownero (14 kata)", "select_backup_file": "Pilih file cadangan", "select_buy_provider_notice": "Pilih penyedia beli di atas. Anda dapat melewatkan layar ini dengan mengatur penyedia pembelian default Anda di pengaturan aplikasi.", "select_destination": "Silakan pilih tujuan untuk file cadangan.", @@ -680,7 +682,7 @@ "show_seed": "Tampilkan seed", "sign_message": "Pesan tanda", "sign_up": "Daftar", - "sign_verify_message": "Tanda tangan atau verifikasi pesan", + "sign_verify_message": "Tanda / verifikasi", "sign_verify_message_sub": "Menandatangani atau memverifikasi pesan menggunakan kunci pribadi Anda", "sign_verify_title": "Tanda / verifikasi", "signature": "Tanda tangan", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 6257d4800..d509cb256 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -299,6 +299,7 @@ "failed_authentication": "Autenticazione fallita. ${state_error}", "faq": "Domande Frequenti", "features": "Caratteristiche", + "fee_less_than_min": "La commissione selezionata è inferiore al minimo, aumenta le commissioni per poter inviare la transazione", "fee_rate": "Tasso di commissione", "fetching": "Recupero", "fiat_api": "Fiat API", @@ -608,6 +609,7 @@ "seedtype_alert_title": "Avviso seedType", "seedtype_legacy": "Legacy (25 parole)", "seedtype_polyseed": "Polyseed (16 parole)", + "seedtype_wownero": "Wownero (14 parole)", "select_backup_file": "Seleziona file di backup", "select_buy_provider_notice": "Seleziona un fornitore di acquisto sopra. È possibile saltare questa schermata impostando il provider di acquisto predefinito nelle impostazioni dell'app.", "select_destination": "Seleziona la destinazione per il file di backup.", @@ -679,7 +681,7 @@ "show_seed": "Mostra seme", "sign_message": "Messaggio di firma", "sign_up": "Registrati", - "sign_verify_message": "Firmare o verificare il messaggio", + "sign_verify_message": "Firmare / verificare", "sign_verify_message_sub": "Firma o verifica un messaggio utilizzando la chiave privata", "sign_verify_title": "Firmare / verificare", "signature": "Firma", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index e30409cdf..f08437336 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -298,6 +298,7 @@ "failed_authentication": "認証失敗. ${state_error}", "faq": "FAQ", "features": "特徴", + "fee_less_than_min": "選択した料金は最低額よりも少ないので、取引を送信できるように料金を引き上げてください", "fee_rate": "料金金利", "fetching": "フェッチング", "fiat_api": "不換紙幣 API", @@ -607,6 +608,7 @@ "seedtype_alert_title": "SeedTypeアラート", "seedtype_legacy": "レガシー(25語)", "seedtype_polyseed": "ポリシード(16語)", + "seedtype_wownero": "wownero(14ワード)", "select_backup_file": "バックアップファイルを選択", "select_buy_provider_notice": "上記の購入プロバイダーを選択してください。デフォルトの購入プロバイダーをアプリ設定で設定して、この画面をスキップできます。", "select_destination": "バックアップファイルの保存先を選択してください。", @@ -678,7 +680,7 @@ "show_seed": "シードを表示", "sign_message": "署名メッセージ", "sign_up": "サインアップ", - "sign_verify_message": "メッセージに署名または確認します", + "sign_verify_message": "署名 /検証", "sign_verify_message_sub": "秘密鍵を使用してメッセージに署名または確認します", "sign_verify_title": "署名 /検証", "signature": "サイン", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 29e68d4c3..60ee05da2 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -298,6 +298,7 @@ "failed_authentication": "인증 실패. ${state_error}", "faq": "FAQ", "features": "특징", + "fee_less_than_min": "선택된 수수료는 최소값보다 적습니다. 거래를 보낼 수있는 수수료를 늘리십시오.", "fee_rate": "수수료", "fetching": "가져 오는 중", "fiat_api": "명목 화폐 API", @@ -607,6 +608,7 @@ "seedtype_alert_title": "종자 경보", "seedtype_legacy": "레거시 (25 단어)", "seedtype_polyseed": "다문 (16 단어)", + "seedtype_wownero": "Wownero (14 단어)", "select_backup_file": "백업 파일 선택", "select_buy_provider_notice": "위의 구매 제공자를 선택하십시오. 앱 설정에서 기본 구매 제공자를 설정 하여이 화면을 건너 뛸 수 있습니다.", "select_destination": "백업 파일의 대상을 선택하십시오.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index d95a1b00d..d0aae18ea 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -298,6 +298,7 @@ "failed_authentication": "အထောက်အထားစိစစ်ခြင်း မအောင်မြင်ပါ။. ${state_error}", "faq": "အမြဲမေးလေ့ရှိသောမေးခွန်းများ", "features": "အင်္ဂါရပ်များ", + "fee_less_than_min": "ရွေးချယ်ထားသည့်အခကြေးငွေသည်အနည်းဆုံးထက်နည်းသည်, ငွေပေးချေမှုကိုပေးပို့နိုင်ရန်အခကြေးငွေကိုတိုးမြှင့်ပေးပါ", "fee_rate": "ကြေးနှုန်း", "fetching": "ခေါ်ယူခြင်း။", "fiat_api": "Fiat API", @@ -606,6 +607,7 @@ "seedtype_alert_title": "ပျိုးပင်သတိပေးချက်", "seedtype_legacy": "အမွေအနှစ် (စကားလုံး 25 လုံး)", "seedtype_polyseed": "polyseed (စကားလုံး 16 လုံး)", + "seedtype_wownero": "Wownero (စကားလုံး 14 လုံး)", "select_backup_file": "အရန်ဖိုင်ကို ရွေးပါ။", "select_buy_provider_notice": "အပေါ်ကဝယ်သူတစ် ဦး ကိုရွေးချယ်ပါ။ သင်၏ default 0 ယ်သူအား app settings တွင် setting လုပ်ခြင်းဖြင့်ဤ screen ကိုကျော်သွားနိုင်သည်။", "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", @@ -677,7 +679,7 @@ "show_seed": "မျိုးစေ့ကိုပြပါ။", "sign_message": "လက်မှတ်စာ", "sign_up": "ဆိုင်းအပ်", - "sign_verify_message": "မက်ဆေ့ခ်ျကိုလက်မှတ်ထိုးသို့မဟုတ်အတည်ပြုရန်", + "sign_verify_message": "လက်မှတ်ထိုး / အတည်ပြုရန်", "sign_verify_message_sub": "သင်၏ကိုယ်ပိုင်သော့ကို သုံး. မက်ဆေ့ခ်ျကိုလက်မှတ်ထိုးပါ", "sign_verify_title": "လက်မှတ်ထိုး / အတည်ပြုရန်", "signature": "လက်မှတ်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 2bb505f3c..273a65ae5 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -298,6 +298,7 @@ "failed_authentication": "Mislukte authenticatie. ${state_error}", "faq": "FAQ", "features": "Functies", + "fee_less_than_min": "Geselecteerde vergoeding is lager dan het minimum, verhoog de kosten om de transactie te kunnen verzenden", "fee_rate": "Tarief", "fetching": "Ophalen", "fiat_api": "Fiat API", @@ -606,6 +607,7 @@ "seedtype_alert_title": "Zaadtype alert", "seedtype_legacy": "Legacy (25 woorden)", "seedtype_polyseed": "Polyseed (16 woorden)", + "seedtype_wownero": "WOWNERO (14 woorden)", "select_backup_file": "Selecteer een back-upbestand", "select_buy_provider_notice": "Selecteer hierboven een koopprovider. U kunt dit scherm overslaan door uw standaard kopenprovider in te stellen in app -instellingen.", "select_destination": "Selecteer de bestemming voor het back-upbestand.", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 09d42e74a..046db2187 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -298,6 +298,7 @@ "failed_authentication": "Nieudane uwierzytelnienie. ${state_error}", "faq": "FAQ", "features": "Cechy", + "fee_less_than_min": "Wybrana opłata jest niższa niż minimum, proszę zwiększyć opłaty, aby móc wysłać transakcję", "fee_rate": "Stawka opłaty", "fetching": "Pobieranie", "fiat_api": "API Walut FIAT", @@ -606,6 +607,7 @@ "seedtype_alert_title": "Ustanowienie typu sedype", "seedtype_legacy": "Dziedzictwo (25 słów)", "seedtype_polyseed": "Poliqueed (16 słów)", + "seedtype_wownero": "Wowero (14 słów)", "select_backup_file": "Wybierz plik kopii zapasowej", "select_buy_provider_notice": "Wybierz powyższe dostawcę zakupu. Możesz pominąć ten ekran, ustawiając domyślnego dostawcę zakupu w ustawieniach aplikacji.", "select_destination": "Wybierz miejsce docelowe dla pliku kopii zapasowej.", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 332623c04..164cb9530 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -298,6 +298,7 @@ "failed_authentication": "Falha na autenticação. ${state_error}", "faq": "FAQ", "features": "Funcionalidades", + "fee_less_than_min": "A taxa selecionada é menor que o mínimo, aumente as taxas para poder enviar a transação", "fee_rate": "Taxa de transação", "fetching": "Buscando", "fiat_api": "API da Fiat", @@ -608,6 +609,7 @@ "seedtype_alert_title": "Alerta de SeedType", "seedtype_legacy": "Legado (25 palavras)", "seedtype_polyseed": "Polyseed (16 palavras)", + "seedtype_wownero": "Wowrone (14 palavras)", "select_backup_file": "Selecione o arquivo de backup", "select_buy_provider_notice": "Selecione um provedor de compra acima. Você pode pular esta tela definindo seu provedor de compra padrão nas configurações de aplicativos.", "select_destination": "Selecione o destino para o arquivo de backup.", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 5b64feb64..28d856191 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -298,6 +298,7 @@ "failed_authentication": "Ошибка аутентификации. ${state_error}", "faq": "FAQ", "features": "Функции", + "fee_less_than_min": "Выбранный сбор меньше, чем минимум, пожалуйста, увеличьте сборы, чтобы быть в состоянии отправить транзакцию", "fee_rate": "Плата", "fetching": "Загрузка", "fiat_api": "Фиат API", @@ -607,6 +608,7 @@ "seedtype_alert_title": "SEEDTYPE ALERT", "seedtype_legacy": "Наследие (25 слов)", "seedtype_polyseed": "Полиса (16 слов)", + "seedtype_wownero": "Wownero (14 слов)", "select_backup_file": "Выберите файл резервной копии", "select_buy_provider_notice": "Выберите поставщика покупки выше. Вы можете пропустить этот экран, установив поставщика покупки по умолчанию в настройках приложения.", "select_destination": "Пожалуйста, выберите место для файла резервной копии.", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 3bd50af5d..ee6d5b7a2 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -298,6 +298,7 @@ "failed_authentication": "การยืนยันสิทธิ์ล้มเหลว ${state_error}", "faq": "คำถามที่พบบ่อย", "features": "คุณสมบัติ", + "fee_less_than_min": "ค่าธรรมเนียมที่เลือกน้อยกว่าขั้นต่ำโปรดเพิ่มค่าธรรมเนียมเพื่อให้สามารถส่งธุรกรรมได้", "fee_rate": "อัตราค่าธรรมเนียม", "fetching": "กำลังโหลด", "fiat_api": "API สกุลเงินตรา", @@ -606,6 +607,7 @@ "seedtype_alert_title": "การแจ้งเตือน seedtype", "seedtype_legacy": "มรดก (25 คำ)", "seedtype_polyseed": "โพลีส (16 คำ)", + "seedtype_wownero": "wownero (14 คำ)", "select_backup_file": "เลือกไฟล์สำรอง", "select_buy_provider_notice": "เลือกผู้ให้บริการซื้อด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการซื้อเริ่มต้นในการตั้งค่าแอป", "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 48914c1e5..2e7b4f4db 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -298,6 +298,7 @@ "failed_authentication": "Nabigo ang pagpapatunay. ${state_error}", "faq": "FAQ", "features": "Mga tampok", + "fee_less_than_min": "Ang napiling bayad ay mas mababa sa minimum, mangyaring dagdagan ang mga bayarin upang maipadala ang transaksyon", "fee_rate": "Rate ng bayad", "fetching": "Pagkuha", "fiat_api": "Fiat API", @@ -606,6 +607,7 @@ "seedtype_alert_title": "Alerto ng Seedtype", "seedtype_legacy": "Legacy (25 na salita)", "seedtype_polyseed": "Polyseed (16 na salita)", + "seedtype_wownero": "Wownero (14 na salita)", "select_backup_file": "Piliin ang backup na file", "select_buy_provider_notice": "Pumili ng provider ng pagbili sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na provider ng pagbili sa mga setting ng app.", "select_destination": "Mangyaring piliin ang patutunguhan para sa backup na file.", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 22f7c8253..fba4a796e 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -298,6 +298,7 @@ "failed_authentication": "Doğrulama başarısız oldu. ${state_error}", "faq": "SSS", "features": "Özellikler", + "fee_less_than_min": "Seçilen ücret asgari düzeyden azdır, lütfen işlemi gönderebilmek için ücretleri artırın", "fee_rate": "Ücret oranı", "fetching": "Getiriliyor", "fiat_api": "İtibari Para API", @@ -606,6 +607,7 @@ "seedtype_alert_title": "SeedType uyarısı", "seedtype_legacy": "Miras (25 kelime)", "seedtype_polyseed": "Polyseed (16 kelime)", + "seedtype_wownero": "Wownero (14 kelime)", "select_backup_file": "Yedek dosyası seç", "select_buy_provider_notice": "Yukarıda bir satın alma sağlayıcısı seçin. App ayarlarında varsayılan satın alma sağlayıcınızı ayarlayarak bu ekranı atlayabilirsiniz.", "select_destination": "Lütfen yedekleme dosyası için hedef seçin.", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 969e46036..ff1f2905f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -298,6 +298,7 @@ "failed_authentication": "Помилка аутентифікації. ${state_error}", "faq": "FAQ", "features": "Особливості", + "fee_less_than_min": "Вибрана плата менша, ніж мінімум, будь ласка, збільште збори, щоб мати можливість відправити транзакцію", "fee_rate": "Ставка плати", "fetching": "Завантаження", "fiat_api": "Фіат API", @@ -607,6 +608,7 @@ "seedtype_alert_title": "Попередження насінника", "seedtype_legacy": "Спадщина (25 слів)", "seedtype_polyseed": "Полісей (16 слів)", + "seedtype_wownero": "Влонеро (14 слів)", "select_backup_file": "Виберіть файл резервної копії", "select_buy_provider_notice": "Виберіть постачальника купівлі вище. Ви можете пропустити цей екран, встановивши свого постачальника купівлі за замовчуванням у налаштуваннях додатків.", "select_destination": "Виберіть місце призначення для файлу резервної копії.", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 6b0906c75..8cfe2ec9f 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -298,6 +298,7 @@ "failed_authentication": "ناکام تصدیق۔ ${state_error}", "faq": "عمومی سوالات", "features": "خصوصیات", + "fee_less_than_min": "منتخب فیس کم سے کم سے کم ہے ، براہ کرم لین دین بھیجنے کے قابل فیس میں اضافہ کریں", "fee_rate": "فیس کی شرح", "fetching": "لا رہا ہے۔", "fiat_api": "Fiat API", @@ -608,6 +609,7 @@ "seedtype_alert_title": "سیڈ ٹائپ الرٹ", "seedtype_legacy": "میراث (25 الفاظ)", "seedtype_polyseed": "پالیسیڈ (16 الفاظ)", + "seedtype_wownero": "واونرو (14 الفاظ)", "select_backup_file": "بیک اپ فائل کو منتخب کریں۔", "select_buy_provider_notice": "اوپر خریدنے والا خریدنے والا منتخب کریں۔ آپ ایپ کی ترتیبات میں اپنے پہلے سے طے شدہ خریدنے والے کو ترتیب دے کر اس اسکرین کو چھوڑ سکتے ہیں۔", "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index cc99dcf97..444c1d6d8 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Thao tác này sẽ xóa ví này. Bạn có muốn tiếp tục không?", "confirm_fee_dedction_content": "Bạn có đồng ý trừ phí từ đầu ra không?", "confirm_fee_deduction": "Xác nhận Khấu trừ Phí", + "confirm_fee_deduction_content": "Bạn có đồng ý khấu trừ phí từ đầu ra không?", "confirm_sending": "Xác nhận gửi", "confirm_silent_payments_switch_node": "Nút hiện tại của bạn không hỗ trợ thanh toán im lặng\\nCake Wallet sẽ chuyển sang một nút tương thích chỉ để quét", "confirmations": "Xác nhận", @@ -161,6 +162,7 @@ "contact_name": "Tên liên hệ", "contact_support": "Liên hệ Hỗ trợ", "continue_text": "Tiếp tục", + "contract_warning": "Địa chỉ hợp đồng này đã được gắn cờ là có khả năng lừa đảo. Vui lòng xử lý một cách thận trọng.", "contractName": "Tên Hợp đồng", "contractSymbol": "Ký hiệu Hợp đồng", "copied_key_to_clipboard": "Đã sao chép ${key} vào khay nhớ tạm", @@ -221,6 +223,7 @@ "displayable": "Có thể hiển thị", "do_not_have_enough_gas_asset": "Bạn không có đủ ${currency} để thực hiện giao dịch với điều kiện mạng blockchain hiện tại. Bạn cần thêm ${currency} để trả phí mạng blockchain, ngay cả khi bạn đang gửi tài sản khác.", "do_not_send": "Đừng gửi", + "do_not_send_funds_to_contract_address_warning": "Không gửi tiền đến địa chỉ này\n\n Đây chỉ là một mã định danh cho mã thông báo, bất kỳ khoản tiền nào được gửi đến địa chỉ này sẽ bị mất.\n\n Lưu ý: Bánh sẽ không bao giờ yêu cầu bạn thêm địa chỉ hợp đồng", "do_not_share_warning_text": "Không chia sẻ điều này với bất kỳ ai, bao gồm cả bộ phận hỗ trợ.\n\nTài sản của bạn có thể và sẽ bị đánh cắp!", "do_not_show_me": "Không hiển thị lại cho tôi", "domain_looks_up": "Tra cứu tên miền", @@ -296,6 +299,8 @@ "failed_authentication": "Xác thực không thành công. ${state_error}", "faq": "FAQ", "features": "Tính năng", + "fee_less_than_min": "Phí được chọn ít hơn mức tối thiểu, vui lòng tăng phí để có thể gửi giao dịch", + "fee_rate": "Tỷ lệ phí", "fetching": "Đang tải", "fiat_api": "API Fiat", "fiat_balance": "Số dư Fiat", @@ -599,6 +604,8 @@ "seed_share": "Chia sẻ hạt giống", "seed_title": "Hạt giống", "seedtype": "Loại hạt giống", + "seedtype_alert_content": "Chia sẻ hạt giống với ví khác chỉ có thể với BIP39 SeedType.", + "seedtype_alert_title": "Cảnh báo hạt giống", "seedtype_legacy": "Di sản (25 từ)", "seedtype_polyseed": "Polyseed (16 từ)", "seedtype_wownero": "Wownero (14 từ)", @@ -612,6 +619,7 @@ "send": "Gửi", "send_address": "Địa chỉ ${cryptoCurrency}", "send_amount": "Số tiền:", + "send_change_to_you": "Thay đổi, cho bạn:", "send_creating_transaction": "Tạo giao dịch", "send_error_currency": "Tiền tệ chỉ có thể chứa số", "send_error_minimum_value": "Giá trị tối thiểu của số tiền là 0.01", @@ -665,6 +673,7 @@ "setup_your_debit_card": "Thiết lập thẻ ghi nợ của bạn", "share": "Chia sẻ", "share_address": "Chia sẻ địa chỉ", + "shared_seed_wallet_groups": "Nhóm ví hạt được chia sẻ", "show_details": "Hiển thị chi tiết", "show_keys": "Hiển thị hạt giống/khóa", "show_market_place": "Hiển thị Thị trường", @@ -678,6 +687,7 @@ "signature_invalid_error": "Chữ ký không hợp lệ cho tin nhắn đã cho", "signTransaction": "Ký giao dịch", "signup_for_card_accept_terms": "Đăng ký thẻ và chấp nhận các điều khoản.", + "silent_payment": "Thanh toán im lặng", "silent_payments": "Thanh toán im lặng", "silent_payments_always_scan": "Đặt Thanh toán im lặng luôn quét", "silent_payments_disclaimer": "Địa chỉ mới không phải là danh tính mới. Đây là việc tái sử dụng một danh tính hiện có với nhãn khác.", @@ -688,6 +698,7 @@ "silent_payments_scanned_tip": "ĐÃ QUÉT ĐỂ TIP! (${tip})", "silent_payments_scanning": "Đang quét thanh toán im lặng", "silent_payments_settings": "Cài đặt thanh toán im lặng", + "single_seed_wallets_group": "Ví hạt đơn", "slidable": "Có thể kéo", "sort_by": "Sắp xếp theo", "spend_key_private": "Khóa chi tiêu (riêng tư)", @@ -710,6 +721,7 @@ "switchToEVMCompatibleWallet": "Vui lòng chuyển sang ví tương thích EVM và thử lại (Ethereum, Polygon)", "symbol": "Ký hiệu", "sync_all_wallets": "Đồng bộ tất cả các ví", + "sync_status_attempting_scan": "Cố gắng quét", "sync_status_attempting_sync": "ĐANG THỬ ĐỒNG BỘ", "sync_status_connected": "ĐÃ KẾT NỐI", "sync_status_connecting": "ĐANG KẾT NỐI", @@ -846,6 +858,7 @@ "view_transaction_on": "Xem giao dịch trên", "voting_weight": "Trọng số bỏ phiếu", "waitFewSecondForTxUpdate": "Vui lòng đợi vài giây để giao dịch được phản ánh trong lịch sử giao dịch", + "wallet_group": "Nhóm ví", "wallet_group_description_four": "Để tạo ra một ví với một hạt giống hoàn toàn mới.", "wallet_group_description_one": "Trong ví bánh, bạn có thể tạo", "wallet_group_description_three": "Để xem ví trên ví và/hoặc màn hình nhóm ví. Hoặc chọn", @@ -854,6 +867,7 @@ "wallet_group_empty_state_text_two": "Dưới đây để làm một cái mới.", "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", "wallet_list_edit_wallet": "Chỉnh sửa ví", "wallet_list_failed_to_load": "Tải ví ${wallet_name} không thành công. ${error}", "wallet_list_failed_to_remove": "Xóa ví ${wallet_name} không thành công. ${error}", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index aa7264b67..15b90b973 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -299,6 +299,7 @@ "failed_authentication": "Ìfẹ̀rílàdí pipòfo. ${state_error}", "faq": "Àwọn ìbéèrè l'a máa ń bèèrè", "features": "Awọn ẹya", + "fee_less_than_min": "Owo ti a ti yan kere ju ti o kere ju, jọwọ mu awọn owo naa pọ si lati ni anfani lati firanṣẹ iṣowo naa", "fee_rate": "Oṣuwọn owo ọya", "fetching": "ń wá", "fiat_api": "Ojú ètò áàpù owó tí ìjọba pàṣẹ wa lò", @@ -607,6 +608,7 @@ "seedtype_alert_title": "Ṣajọpọ Seeytype", "seedtype_legacy": "Legacy (awọn ọrọ 25)", "seedtype_polyseed": "Polyseed (awọn ọrọ 16)", + "seedtype_wownero": "Wowero (awọn ọrọ 14)", "select_backup_file": "Select backup file", "select_buy_provider_notice": "Yan olupese Ra loke. O le skii iboju yii nipa ṣiṣeto olupese rẹ ni awọn eto App.", "select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 0306f8ad6..ae7e15132 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -298,6 +298,7 @@ "failed_authentication": "身份验证失败. ${state_error}", "faq": "FAQ", "features": "特征", + "fee_less_than_min": "选定费用小于最低费用,请增加费用以发送交易", "fee_rate": "费率", "fetching": "正在获取", "fiat_api": "法币API", @@ -606,6 +607,7 @@ "seedtype_alert_title": "籽粒警报", "seedtype_legacy": "遗产(25个单词)", "seedtype_polyseed": "多种物品(16个单词)", + "seedtype_wownero": "沃恩罗(14个单词)", "select_backup_file": "选择备份文件", "select_buy_provider_notice": "在上面选择买入提供商。您可以通过在应用程序设置中设置默认的购买提供商来跳过此屏幕。", "select_destination": "请选择备份文件的目的地。", From 17de9f16b87425885c39b52f392abe45ba9ff3a2 Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 23 Sep 2024 19:17:07 +0300 Subject: [PATCH 088/142] fix polygon name (#1697) --- lib/exchange/provider/stealth_ex_exchange_provider.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index 601735595..8fa72aeaa 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -284,6 +284,7 @@ class StealthExExchangeProvider extends ExchangeProvider { String _getName(CryptoCurrency currency) { if (currency == CryptoCurrency.usdcEPoly) return 'usdce'; + if (currency == CryptoCurrency.maticpoly) return 'matic'; return currency.title.toLowerCase(); } From bab45a9e36b2e017f9649fc98a7f8d7fe278c764 Mon Sep 17 00:00:00 2001 From: cyan Date: Mon, 23 Sep 2024 18:18:21 +0200 Subject: [PATCH 089/142] add missing error messages (#1696) --- cw_bitcoin/lib/exceptions.dart | 15 +++++++++++++++ cw_core/lib/exceptions.dart | 5 +++++ .../api/exceptions/setup_wallet_exception.dart | 5 +++++ .../api/exceptions/setup_wallet_exception.dart | 3 +++ .../wallet_restore_from_keys_exception.dart | 3 +++ .../wallet_restore_from_seed_exception.dart | 3 +++ .../api/exceptions/setup_wallet_exception.dart | 3 +++ .../wallet_restore_from_keys_exception.dart | 2 ++ 8 files changed, 39 insertions(+) diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart index 575a61b0b..f7c593135 100644 --- a/cw_bitcoin/lib/exceptions.dart +++ b/cw_bitcoin/lib/exceptions.dart @@ -3,6 +3,11 @@ import 'package:cw_core/exceptions.dart'; class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException { BitcoinTransactionWrongBalanceException({super.amount}) : super(CryptoCurrency.btc); + + @override + String toString() { + return "BitcoinTransactionWrongBalanceException: $amount, $currency"; + } } class BitcoinTransactionNoInputsException extends TransactionNoInputsException {} @@ -13,10 +18,20 @@ class BitcoinTransactionNoDustException extends TransactionNoDustException {} class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChangeException { BitcoinTransactionNoDustOnChangeException(super.max, super.min); + + @override + String toString() { + return "BitcoinTransactionNoDustOnChangeException: max: $max, min: $min"; + } } class BitcoinTransactionCommitFailed extends TransactionCommitFailed { BitcoinTransactionCommitFailed({super.errorMessage}); + + @override + String toString() { + return errorMessage??"unknown error"; + } } class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {} diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index c13861698..80bdd2886 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -24,6 +24,11 @@ class TransactionCommitFailed implements Exception { final String? errorMessage; TransactionCommitFailed({this.errorMessage}); + + @override + String toString() { + return errorMessage??"unknown error"; + } } class TransactionCommitFailedDustChange implements Exception {} diff --git a/cw_haven/lib/api/exceptions/setup_wallet_exception.dart b/cw_haven/lib/api/exceptions/setup_wallet_exception.dart index b6e0c1f18..9d985665b 100644 --- a/cw_haven/lib/api/exceptions/setup_wallet_exception.dart +++ b/cw_haven/lib/api/exceptions/setup_wallet_exception.dart @@ -2,4 +2,9 @@ class SetupWalletException implements Exception { SetupWalletException({required this.message}); final String message; + + @override + String toString() { + return message; + } } \ No newline at end of file diff --git a/cw_monero/lib/api/exceptions/setup_wallet_exception.dart b/cw_monero/lib/api/exceptions/setup_wallet_exception.dart index b6e0c1f18..c0d25c62f 100644 --- a/cw_monero/lib/api/exceptions/setup_wallet_exception.dart +++ b/cw_monero/lib/api/exceptions/setup_wallet_exception.dart @@ -2,4 +2,7 @@ class SetupWalletException implements Exception { SetupWalletException({required this.message}); final String message; + + @override + String toString() => message; } \ No newline at end of file diff --git a/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart index c6b6c6ef7..6c461ee4c 100644 --- a/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -2,4 +2,7 @@ class WalletRestoreFromKeysException implements Exception { WalletRestoreFromKeysException({required this.message}); final String message; + + @override + String toString() => message; } \ No newline at end of file diff --git a/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart b/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart index 004cd7958..904a7e767 100644 --- a/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart +++ b/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart @@ -2,4 +2,7 @@ class WalletRestoreFromSeedException implements Exception { WalletRestoreFromSeedException({required this.message}); final String message; + + @override + String toString() => message; } \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart b/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart index b6e0c1f18..c0d25c62f 100644 --- a/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart +++ b/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart @@ -2,4 +2,7 @@ class SetupWalletException implements Exception { SetupWalletException({required this.message}); final String message; + + @override + String toString() => message; } \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart index c6b6c6ef7..ad576faa2 100644 --- a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -2,4 +2,6 @@ class WalletRestoreFromKeysException implements Exception { WalletRestoreFromKeysException({required this.message}); final String message; + + String toString() => message; } \ No newline at end of file From 094b5ec82e2ca748788f4897794b685626e35872 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 23 Sep 2024 20:13:40 +0300 Subject: [PATCH 090/142] Revert "fix polygon name (#1697)" (#1699) This reverts commit 17de9f16b87425885c39b52f392abe45ba9ff3a2. --- lib/exchange/provider/stealth_ex_exchange_provider.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index 8fa72aeaa..601735595 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -284,7 +284,6 @@ class StealthExExchangeProvider extends ExchangeProvider { String _getName(CryptoCurrency currency) { if (currency == CryptoCurrency.usdcEPoly) return 'usdce'; - if (currency == CryptoCurrency.maticpoly) return 'matic'; return currency.title.toLowerCase(); } From cf1e8a306c3117feb3be42c81c5abe0fd2fe7769 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 24 Sep 2024 06:40:33 +0300 Subject: [PATCH 091/142] Qr code passphrase restoration flow fix (#1694) * add passphrase to credentials * prevent double restoring * fix conflict, simplify restricting user from tapping restore multiple times --------- Co-authored-by: Omar Hatem --- .../screens/restore/restore_options_page.dart | 96 ++++++++++++------- .../widgets/alert_with_no_action.dart.dart | 30 ++++++ lib/src/widgets/base_alert_dialog.dart | 4 +- .../restore/restore_from_qr_vm.dart | 8 +- lib/view_model/restore/restore_wallet.dart | 4 + 5 files changed, 100 insertions(+), 42 deletions(-) create mode 100644 lib/src/widgets/alert_with_no_action.dart.dart diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 472f311c9..d671230c4 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -31,30 +31,45 @@ class RestoreOptionsPage extends BasePage { final bool isNewInstall; + @override + Widget body(BuildContext context) { + return _RestoreOptionsBody(isNewInstall: isNewInstall, themeType: currentTheme.type); + } +} + +class _RestoreOptionsBody extends StatefulWidget { + const _RestoreOptionsBody({required this.isNewInstall, required this.themeType}); + + final bool isNewInstall; + final ThemeType themeType; + + @override + _RestoreOptionsBodyState createState() => _RestoreOptionsBodyState(); +} + +class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { + bool isRestoring = false; + bool get _doesSupportHardwareWallets { if (!DeviceInfo.instance.isMobile) { return false; } if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) - .isNotEmpty; + return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS).isNotEmpty; } return true; } @override - Widget body(BuildContext context) { + Widget build(BuildContext context) { final mainImageColor = Theme.of(context).extension()!.pageTitleTextColor; final brightImageColor = Theme.of(context).extension()!.textColor; - final imageColor = currentTheme.type == ThemeType.bright ? brightImageColor : mainImageColor; + final imageColor = widget.themeType == ThemeType.bright ? brightImageColor : mainImageColor; final imageLedger = Image.asset('assets/images/ledger_nano.png', width: 40, color: imageColor); final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor); final imageBackup = Image.asset('assets/images/backup.png', color: imageColor); - final qrCode = Image.asset('assets/images/restore_qr.png', color: imageColor); - - return Center( child: Container( @@ -66,16 +81,17 @@ class RestoreOptionsPage extends BasePage { children: [ OptionTile( key: ValueKey('restore_options_from_seeds_button_key'), - onPressed: () => Navigator.pushNamed( - context, - Routes.restoreWalletFromSeedKeys, - arguments: isNewInstall, - ), + onPressed: () => + Navigator.pushNamed( + context, + Routes.restoreWalletFromSeedKeys, + arguments: widget.isNewInstall, + ), image: imageSeedKeys, title: S.of(context).restore_title_from_seed_keys, description: S.of(context).restore_description_from_seed_keys, ), - if (isNewInstall) + if (widget.isNewInstall) Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( @@ -91,9 +107,8 @@ class RestoreOptionsPage extends BasePage { padding: EdgeInsets.only(top: 24), child: OptionTile( key: ValueKey('restore_options_from_hardware_wallet_button_key'), - onPressed: () => Navigator.pushNamed( - context, Routes.restoreWalletFromHardwareWallet, - arguments: isNewInstall), + onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet, + arguments: widget.isNewInstall), image: imageLedger, title: S.of(context).restore_title_from_hardware_wallet, description: S.of(context).restore_description_from_hardware_wallet, @@ -119,36 +134,47 @@ class RestoreOptionsPage extends BasePage { } void _onWalletCreateFailure(BuildContext context, String error) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.current.error, - alertContent: error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); + setState(() { + isRestoring = false; + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + } Future _onScanQRCode(BuildContext context) async { - final isCameraPermissionGranted = - await PermissionHandler.checkPermission(Permission.camera, context); + final isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; bool isPinSet = false; - if (isNewInstall) { + if (widget.isNewInstall) { await Navigator.pushNamed(context, Routes.setupPin, arguments: (PinCodeState setupPinContext, String _) { - setupPinContext.close(); - isPinSet = true; - }); + setupPinContext.close(); + isPinSet = true; + }); } - if (!isNewInstall || isPinSet) { + if (!widget.isNewInstall || isPinSet) { try { + if (isRestoring) { + return; + } + setState(() { + isRestoring = true; + }); final restoreWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); - final restoreFromQRViewModel = - getIt.get(param1: restoreWallet.type); + final restoreFromQRViewModel = getIt.get(param1: restoreWallet.type); await restoreFromQRViewModel.create(restoreWallet: restoreWallet); if (restoreFromQRViewModel.state is FailureState) { @@ -160,4 +186,4 @@ class RestoreOptionsPage extends BasePage { } } } -} +} \ No newline at end of file diff --git a/lib/src/widgets/alert_with_no_action.dart.dart b/lib/src/widgets/alert_with_no_action.dart.dart new file mode 100644 index 000000000..623656397 --- /dev/null +++ b/lib/src/widgets/alert_with_no_action.dart.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; + +class AlertWithNoAction extends BaseAlertDialog { + AlertWithNoAction({ + required this.alertTitle, + required this.alertContent, + this.alertBarrierDismissible = true, + Key? key, + }); + + final String alertTitle; + final String alertContent; + final bool alertBarrierDismissible; + + @override + String get titleText => alertTitle; + + @override + String get contentText => alertContent; + + @override + bool get barrierDismissible => alertBarrierDismissible; + + @override + bool get isBottomDividerExists => false; + + @override + Widget actionButtons(BuildContext context) => Container(height: 60); +} diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 2e6f1571e..1b521a427 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -17,6 +17,8 @@ class BaseAlertDialog extends StatelessWidget { bool get isDividerExists => false; + bool get isBottomDividerExists => true; + VoidCallback get actionLeft => () {}; VoidCallback get actionRight => () {}; @@ -205,7 +207,7 @@ class BaseAlertDialog extends StatelessWidget { ) ], ), - const HorizontalSectionDivider(), + if (isBottomDividerExists) const HorizontalSectionDivider(), ClipRRect( borderRadius: BorderRadius.all(Radius.circular(30)), child: actionButtons(context)) diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index bb7d0f8d1..6701b639d 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -56,12 +56,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store WalletCredentials getCredentialsFromRestoredWallet( dynamic options, RestoredWallet restoreWallet) { final password = generateWalletPassword(); - String? passphrase; + DerivationInfo? derivationInfo; - if (options != null) { - derivationInfo = options["derivationInfo"] as DerivationInfo?; - passphrase = options["passphrase"] as String?; - } derivationInfo ??= getDefaultCreateDerivation(); switch (restoreWallet.restoreMode) { @@ -119,7 +115,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, - passphrase: passphrase, + passphrase: restoreWallet.passphrase, derivationType: derivationInfo!.derivationType!, derivationPath: derivationInfo.derivationPath!, ); diff --git a/lib/view_model/restore/restore_wallet.dart b/lib/view_model/restore/restore_wallet.dart index d46c48092..2c2a25005 100644 --- a/lib/view_model/restore/restore_wallet.dart +++ b/lib/view_model/restore/restore_wallet.dart @@ -10,6 +10,7 @@ class RestoredWallet { this.spendKey, this.viewKey, this.mnemonicSeed, + this.passphrase, this.txAmount, this.txDescription, this.recipientName, @@ -23,6 +24,7 @@ class RestoredWallet { final String? spendKey; final String? viewKey; final String? mnemonicSeed; + final String? passphrase; final String? txAmount; final String? txDescription; final String? recipientName; @@ -46,11 +48,13 @@ class RestoredWallet { final height = json['height'] as String?; final mnemonic_seed = json['mnemonic_seed'] as String?; final seed = json['seed'] as String? ?? json['hexSeed'] as String?; + final passphrase = json['passphrase'] as String?; return RestoredWallet( restoreMode: json['mode'] as WalletRestoreMode, type: json['type'] as WalletType, address: json['address'] as String?, mnemonicSeed: mnemonic_seed ?? seed, + passphrase: passphrase, height: height != null ? int.parse(height) : 0, ); } From fc7bea683034251900d950cf8cfd6cfeb809dad4 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:37:09 +0100 Subject: [PATCH 092/142] Fix-agressive-contract-check (#1698) * fix: Lower aggressiveness and modify parameter used when fetching * fix: Remove extra warning text when adding contract address * chore: Add some tracking logs * chore: Add some tracking logs * chore: More comprehensive logs * chore: More readable release logs --------- Co-authored-by: Omar Hatem --- .../src/pending_bitcoin_cash_transaction.dart | 4 ++- .../screens/dashboard/edit_token_page.dart | 9 ------ .../dashboard/home_settings_view_model.dart | 32 ++++++++++++------- res/values/strings_ar.arb | 1 - res/values/strings_bg.arb | 1 - res/values/strings_cs.arb | 1 - res/values/strings_de.arb | 1 - res/values/strings_en.arb | 1 - res/values/strings_es.arb | 1 - res/values/strings_fr.arb | 1 - res/values/strings_ha.arb | 1 - res/values/strings_hi.arb | 1 - res/values/strings_hr.arb | 1 - res/values/strings_hy.arb | 1 - res/values/strings_id.arb | 1 - res/values/strings_it.arb | 1 - res/values/strings_ja.arb | 1 - res/values/strings_ko.arb | 1 - res/values/strings_my.arb | 1 - res/values/strings_nl.arb | 1 - res/values/strings_pl.arb | 1 - res/values/strings_pt.arb | 1 - res/values/strings_ru.arb | 1 - res/values/strings_th.arb | 1 - res/values/strings_tl.arb | 1 - res/values/strings_tr.arb | 1 - res/values/strings_uk.arb | 1 - res/values/strings_ur.arb | 1 - res/values/strings_vi.arb | 1 - res/values/strings_yo.arb | 1 - res/values/strings_zh.arb | 1 - 31 files changed, 23 insertions(+), 50 deletions(-) diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart index 6d2ab4696..e1fa9d6e0 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -82,5 +82,7 @@ class PendingBitcoinCashTransaction with PendingTransaction { date: DateTime.now(), isPending: true, confirmations: 0, - fee: fee); + fee: fee, + isReplaced: false, + ); } diff --git a/lib/src/screens/dashboard/edit_token_page.dart b/lib/src/screens/dashboard/edit_token_page.dart index 6f662e2be..a1c429ad8 100644 --- a/lib/src/screens/dashboard/edit_token_page.dart +++ b/lib/src/screens/dashboard/edit_token_page.dart @@ -170,15 +170,6 @@ class _EditTokenPageBodyState extends State { bottomSection: Column( children: [ if (_showDisclaimer) ...[ - Text( - S.current.do_not_send_funds_to_contract_address_warning, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, - color: Theme.of(context).extension()!.titleColor, - ), - ), - SizedBox(height: 20), CheckboxWidget( value: _disclaimerChecked, caption: S.of(context).add_token_disclaimer_check, diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index bb21f20ef..4269078a5 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -151,24 +151,34 @@ abstract class HomeSettingsViewModelBase with Store { bool isEthereum = _balanceViewModel.wallet.type == WalletType.ethereum; + print('An extra log for now'); + bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis( contractAddress, isEthereum ? 'eth' : 'polygon', ); + print('Is Potential Scam from Moralis: $isPotentialScamViaMoralis'); + bool isPotentialScamViaExplorers = await _isPotentialScamTokenViaExplorers( contractAddress, isEthereum: isEthereum, ); + print('Is Potential Scam from Explorers: $isPotentialScamViaExplorers'); + bool isUnverifiedContract = await _isContractUnverified( contractAddress, isEthereum: isEthereum, ); + print('Is Unverified Contract: $isUnverifiedContract'); + final showWarningForContractAddress = isPotentialScamViaMoralis || isUnverifiedContract || isPotentialScamViaExplorers; + print('Show Warning: $showWarningForContractAddress'); + return showWarningForContractAddress; } finally { isValidatingContractAddress = false; @@ -236,6 +246,7 @@ abstract class HomeSettingsViewModelBase with Store { return false; } catch (e) { + print('Error while checking scam via moralis: ${e.toString()}'); return true; } } @@ -261,29 +272,22 @@ abstract class HomeSettingsViewModelBase with Store { final decodedResponse = jsonDecode(response.body) as Map; if (decodedResponse['status'] != '1') { - log('${decodedResponse['result']}'); + print('${response.body}\n'); + print('${decodedResponse['result']}\n'); return true; } final tokenInfo = Erc20TokenInfoExplorers.fromJson(decodedResponse['result'][0] as Map); - // A token without an email to reach its creators is a potential red flag - if (tokenInfo.email?.isEmpty == true) { - return true; - } - // A token without a website is a potential red flag if (tokenInfo.website?.isEmpty == true) { return true; } - // if (tokenInfo.whitepaper == null) { - // return true; - // } - return false; } catch (e) { + print('Error while checking scam via explorers: ${e.toString()}'); return true; } } @@ -298,7 +302,7 @@ abstract class HomeSettingsViewModelBase with Store { { "module": "contract", "action": "getsourcecode", - "contractaddress": contractAddress, + "address": contractAddress, "apikey": isEthereum ? secrets.etherScanApiKey : secrets.polygonScanApiKey, }, ); @@ -309,17 +313,21 @@ abstract class HomeSettingsViewModelBase with Store { final decodedResponse = jsonDecode(response.body) as Map; if (decodedResponse['status'] == '0') { - log('${decodedResponse['result']}'); + print('${response.body}\n'); + print('${decodedResponse['result']}\n'); return true; } if (decodedResponse['status'] == '1' && decodedResponse['result'][0]['ABI'] == 'Contract source code not verified') { + print('Call is valid but contract is not verified'); return true; // Contract is not verified } else { + print('Call is valid and contract is verified'); return false; // Contract is verified } } catch (e) { + print('Error while checking contract verification: ${e.toString()}'); return true; } } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 12ecceed9..c9a0c1a37 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -222,7 +222,6 @@ "displayable": "قابل للعرض", "do_not_have_enough_gas_asset": "ليس لديك ما يكفي من ${currency} لإجراء معاملة وفقًا لشروط شبكة blockchain الحالية. أنت بحاجة إلى المزيد من ${currency} لدفع رسوم شبكة blockchain، حتى لو كنت ترسل أصلًا مختلفًا.", "do_not_send": "لا ترسل", - "do_not_send_funds_to_contract_address_warning": "لا ترسل أموالًا إلى هذا العنوان \n\n هذا مجرد معرف للرمز المميز ، فستضيع أي أموال تم إرسالها إلى هذا العنوان. \n\n ملاحظة: لن تطلب منك Cake إضافة عنوان عقد أبدًا", "do_not_share_warning_text": "لا تشارك هذه مع أي شخص آخر ، بما في ذلك الدعم.\n\nيمكن أن تتم سرقة أموالك!", "do_not_show_me": "لا ترني هذا مجددا", "domain_looks_up": "ﻝﺎﺠﻤﻟﺍ ﺚﺤﺑ ﺕﺎﻴﻠﻤﻋ", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index b1a1096e6..2021dd220 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -222,7 +222,6 @@ "displayable": "Възможност за показване", "do_not_have_enough_gas_asset": "Нямате достатъчно ${currency}, за да извършите транзакция с текущите условия на блокчейн мрежата. Имате нужда от повече ${currency}, за да платите таксите за блокчейн мрежа, дори ако изпращате различен актив.", "do_not_send": "Не изпращай", - "do_not_send_funds_to_contract_address_warning": "Не изпращайте средства на този адрес \n\n Това е само идентификатор за токена, всички средства, изпратени на този адрес", "do_not_share_warning_text": "Не споделяйте това с никого, дори и отдел поддръжка.\n\nПарите Ви могат и ще бъдат откраднати!", "do_not_show_me": "Не показвай повече това", "domain_looks_up": "Търсене на домейни", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 7ce797845..bc8a8903a 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -222,7 +222,6 @@ "displayable": "Zobrazitelné", "do_not_have_enough_gas_asset": "Nemáte dostatek ${currency} k provedení transakce s aktuálními podmínkami blockchainové sítě. K placení poplatků za blockchainovou síť potřebujete více ${currency}, i když posíláte jiné aktivum.", "do_not_send": "Neodesílat", - "do_not_send_funds_to_contract_address_warning": "Neposílejte finanční prostředky na tuto adresu \n\n Toto je pouze identifikátor pro token, jakékoli prostředky zaslané na tuto adresu budou ztraceny.", "do_not_share_warning_text": "Toto nesdílejte s nikým jiným, ani s podporou.\n\nJinak mohou být Vaše prostředky ukradeny!", "do_not_show_me": "Příště nezobrazovat", "domain_looks_up": "Vyhledávání domén", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index e599da769..4a7565d0c 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -222,7 +222,6 @@ "displayable": "Anzeigebar", "do_not_have_enough_gas_asset": "Sie verfügen nicht über genügend ${currency}, um eine Transaktion unter den aktuellen Bedingungen des Blockchain-Netzwerks durchzuführen. Sie benötigen mehr ${currency}, um die Gebühren für das Blockchain-Netzwerk zu bezahlen, auch wenn Sie einen anderen Vermögenswert senden.", "do_not_send": "Nicht senden", - "do_not_send_funds_to_contract_address_warning": "Senden Sie keine Mittel an diese Adresse \n\n Dies ist nur eine Kennung für das Token. Alle an diese Adresse gesendeten Mittel gehen verloren. \n\n Hinweis: Kuchen würde Sie niemals auffordern, eine Vertragsadresse hinzuzufügen", "do_not_share_warning_text": "Teilen Sie diese nicht mit anderen, einschließlich Support.\n\nIhr Geld kann und wird gestohlen werden!", "do_not_show_me": "Zeig mir das nicht noch einmal", "domain_looks_up": "Domain-Suchen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 72e05b8b5..975667036 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -222,7 +222,6 @@ "displayable": "Displayable", "do_not_have_enough_gas_asset": "You do not have enough ${currency} to make a transaction with the current blockchain network conditions. You need more ${currency} to pay blockchain network fees, even if you are sending a different asset.", "do_not_send": "Don't send", - "do_not_send_funds_to_contract_address_warning": "Do not send funds to this address\n\n This is just an identifier for the token, any funds sent to this address will be lost.\n\n NOTE: Cake would never ask you to add a contract address", "do_not_share_warning_text": "Do not share these with anyone else, including support.\n\nYour funds can and will be stolen!", "do_not_show_me": "Do not show me this again", "domain_looks_up": "Domain lookups", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 2cdc2318a..19356fc20 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -222,7 +222,6 @@ "displayable": "Visualizable", "do_not_have_enough_gas_asset": "No tienes suficiente ${currency} para realizar una transacción con las condiciones actuales de la red blockchain. Necesita más ${currency} para pagar las tarifas de la red blockchain, incluso si envía un activo diferente.", "do_not_send": "no enviar", - "do_not_send_funds_to_contract_address_warning": "No envíe fondos a esta dirección \n\n Esto es solo un identificador para el token, se perderán los fondos enviados a esta dirección. \n\n Nota: Cake nunca le pediría que agregue una dirección de contrato", "do_not_share_warning_text": "No comparta estos con nadie más, incluido el soporte.\n\n¡Sus fondos pueden ser y serán robados!", "do_not_show_me": "no me muestres esto otra vez", "domain_looks_up": "Búsquedas de dominio", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 27109c0ae..4b57d026b 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -222,7 +222,6 @@ "displayable": "Visible", "do_not_have_enough_gas_asset": "Vous n'avez pas assez de ${currency} pour effectuer une transaction avec les conditions actuelles du réseau blockchain. Vous avez besoin de plus de ${currency} pour payer les frais du réseau blockchain, même si vous envoyez un actif différent.", "do_not_send": "Ne pas envoyer", - "do_not_send_funds_to_contract_address_warning": "N'envoyez pas de fonds à cette adresse \n\n Ceci est juste un identifiant pour le jeton, tous les fonds envoyés à cette adresse seront perdus. \n\n Remarque: Le gâteau ne vous demanderait jamais d'ajouter une adresse de contrat", "do_not_share_warning_text": "Ne les partagez avec personne, y compris avec l'assistance.\n\nVos fonds seraient inmanquablement volés !", "do_not_show_me": "Ne plus me montrer ceci à l'avenir", "domain_looks_up": "Résolution de nom", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 1c293bd54..34cbf0a69 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -222,7 +222,6 @@ "displayable": "Ana iya nunawa", "do_not_have_enough_gas_asset": "Ba ku da isassun ${currency} don yin ma'amala tare da yanayin cibiyar sadarwar blockchain na yanzu. Kuna buƙatar ƙarin ${currency} don biyan kuɗaɗen cibiyar sadarwar blockchain, koda kuwa kuna aika wata kadara daban.", "do_not_send": "Kada ka aika", - "do_not_send_funds_to_contract_address_warning": "Kada ku aika da kudade zuwa wannan adireshin \n\n Wannan kawai mai ganowa ne kawai don token, kowane asusu da aka aiko zuwa wannan adireshin za a rasa. # Lafazin ba zai taba tambayar ka ƙara adireshin kwangila", "do_not_share_warning_text": "Kada ku raba waɗannan ga kowa, gami da tallafi.\n\nZa a iya sace kuɗin ku kuma za a sace!", "do_not_show_me": "Kar ka sake nuna min wannan", "domain_looks_up": "Binciken yanki", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index a93e20ea0..e321358ee 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -222,7 +222,6 @@ "displayable": "प्रदर्शन योग्य", "do_not_have_enough_gas_asset": "वर्तमान ब्लॉकचेन नेटवर्क स्थितियों में लेनदेन करने के लिए आपके पास पर्याप्त ${currency} नहीं है। ब्लॉकचेन नेटवर्क शुल्क का भुगतान करने के लिए आपको अधिक ${currency} की आवश्यकता है, भले ही आप एक अलग संपत्ति भेज रहे हों।", "do_not_send": "मत भेजो", - "do_not_send_funds_to_contract_address_warning": "इस पते पर धन न भेजें \n\n यह सिर्फ टोकन के लिए एक पहचानकर्ता है, इस पते पर भेजे गए किसी भी धन को खो दिया जाएगा। \n\n नोट: केक आपको एक अनुबंध पता जोड़ने के लिए कभी नहीं कहेगा", "do_not_share_warning_text": "समर्थन सहित, इन्हें किसी और के साथ साझा न करें।\n\nआपके धन की चोरी हो सकती है और होगी!", "do_not_show_me": "मुझे यह फिर न दिखाएं", "domain_looks_up": "डोमेन लुकअप", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index b60a056fd..cd91d5291 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -222,7 +222,6 @@ "displayable": "Dostupno za prikaz", "do_not_have_enough_gas_asset": "Nemate dovoljno ${currency} da izvršite transakciju s trenutačnim uvjetima blockchain mreže. Trebate više ${currency} da platite naknade za blockchain mrežu, čak i ako šaljete drugu imovinu.", "do_not_send": "Ne šalji", - "do_not_send_funds_to_contract_address_warning": "Ne šaljite sredstva na ovu adresu \n\n Ovo je samo identifikator za token, izgubit će se bilo koja sredstva poslana na ovu adresu. \n\n Napomena: Torta nikad ne bi tražila da dodate adresu ugovora", "do_not_share_warning_text": "Nemojte ih dijeliti ni s kim, uključujući podršku.\n\nVaša sredstva mogu i bit će ukradena!", "do_not_show_me": "Ne pokazuj mi ovo više", "domain_looks_up": "Pretraga domena", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index eeb1d3f99..8c65817c2 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -222,7 +222,6 @@ "displayable": "Ցուցադրվող", "do_not_have_enough_gas_asset": "Դուք չունեք բավարար ${currency} տրանզակցիան կատարելու համար ընթացիկ բլոկչեյն ցանցի պայմաններում: Դուք պետք է ունենաք ավելի շատ ${currency} blockchain ցանցի միջնորդավճարները վճարելու համար, նույնիսկ եթե դուք այլ ակտիվ եք ուղարկում:", "do_not_send": "Մի ուղարկեք", - "do_not_send_funds_to_contract_address_warning": "Այս հասցեին գումարներ մի ուղարկեք \n\n Սա պարզապես նույնականացն է նշանի համար, այս հասցեով ուղարկված ցանկացած միջոց կկորչի: \n\n Նշում. Տորթը երբեք չի խնդրի ձեզ ավելացնել պայմանագրի հասցե", "do_not_share_warning_text": "Մի կիսեք այս տեղեկատվությունը որևէ մեկի հետ, այդ թվում նաև աջակցության հետ: \n\nՁեր միջոցները կարող են գողանալ կորցնել!", "do_not_show_me": "Մի ցուցադրեք ինձ նորից", "domain_looks_up": "Դոմեյնի որոնում", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 5e48ff1d3..a888a336a 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -222,7 +222,6 @@ "displayable": "Dapat ditampilkan", "do_not_have_enough_gas_asset": "Anda tidak memiliki cukup ${currency} untuk melakukan transaksi dengan kondisi jaringan blockchain saat ini. Anda memerlukan lebih banyak ${currency} untuk membayar biaya jaringan blockchain, meskipun Anda mengirimkan aset yang berbeda.", "do_not_send": "Jangan kirim", - "do_not_send_funds_to_contract_address_warning": "Jangan mengirim dana ke alamat ini \n\n Ini hanya pengidentifikasi untuk token, dana apa pun yang dikirim ke alamat ini akan hilang. \n\n Catatan: Kue tidak akan pernah meminta Anda untuk menambahkan alamat kontrak", "do_not_share_warning_text": "Jangan berikan ini pada siapapun, termasuk dukungan.\n\nDana Anda bisa dan akan dicuri!", "do_not_show_me": "Jangan tampilkan ini lagi", "domain_looks_up": "Pencarian domain", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index d509cb256..0e307ac9f 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -223,7 +223,6 @@ "displayable": "Visualizzabile", "do_not_have_enough_gas_asset": "Non hai abbastanza ${currency} per effettuare una transazione con le attuali condizioni della rete blockchain. Hai bisogno di più ${currency} per pagare le commissioni della rete blockchain, anche se stai inviando una risorsa diversa.", "do_not_send": "Non inviare", - "do_not_send_funds_to_contract_address_warning": "Non inviare fondi a questo indirizzo \n\n Questo è solo un identificatore per il token, qualsiasi fondi inviati a questo indirizzo andrà perso. \n\n Nota: la torta non ti chiederebbe mai di aggiungere un indirizzo contrattuale", "do_not_share_warning_text": "Non condividerli con nessun altro, incluso il supporto.\n\nI tuoi fondi possono e saranno rubati!", "do_not_show_me": "Non mostrarmelo di nuovo", "domain_looks_up": "Ricerche di domini", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index f08437336..efb7bcc81 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -222,7 +222,6 @@ "displayable": "表示可能", "do_not_have_enough_gas_asset": "現在のブロックチェーン ネットワークの状況では、トランザクションを行うのに十分な ${currency} がありません。別のアセットを送信する場合でも、ブロックチェーン ネットワーク料金を支払うにはさらに ${currency} が必要です。", "do_not_send": "送信しない", - "do_not_send_funds_to_contract_address_warning": "この住所に資金を送らないでください\n\nこれはトークンの識別子であり、この住所に送られた資金は失われます。", "do_not_share_warning_text": "サポートを含め、これらを他の誰とも共有しないでください。\n\nあなたの資金は盗まれる可能性があります!", "do_not_show_me": "また僕にこれを見せないでください", "domain_looks_up": "ドメイン検索", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 60ee05da2..24b6acc8c 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -222,7 +222,6 @@ "displayable": "표시 가능", "do_not_have_enough_gas_asset": "현재 블록체인 네트워크 조건으로 거래를 하기에는 ${currency}이(가) 충분하지 않습니다. 다른 자산을 보내더라도 블록체인 네트워크 수수료를 지불하려면 ${currency}가 더 필요합니다.", "do_not_send": "보내지 마세요", - "do_not_send_funds_to_contract_address_warning": "이 주소로 자금을 보내지 마십시오 \n\n 이것은 토큰의 식별자 일뿐입니다.이 주소로 전송 된 모든 자금은 손실됩니다. \n\n 참고 : Cake는 계약서 주소를 추가하도록 요구하지 않습니다.", "do_not_share_warning_text": "지원을 포함하여 다른 사람과 이러한 정보를 공유하지 마십시오.\n\n귀하의 자금은 도난당할 수 있고 도난당할 수 있습니다!", "do_not_show_me": "나를 다시 표시하지 않음", "domain_looks_up": "도메인 조회", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index d0aae18ea..a2d783e52 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -222,7 +222,6 @@ "displayable": "ပြသနိုင်သည်။", "do_not_have_enough_gas_asset": "လက်ရှိ blockchain ကွန်ရက်အခြေအနေများနှင့် အရောင်းအဝယ်ပြုလုပ်ရန် သင့်တွင် ${currency} လုံလောက်မှုမရှိပါ။ သင်သည် မတူညီသော ပိုင်ဆိုင်မှုတစ်ခုကို ပေးပို့နေသော်လည်း blockchain ကွန်ရက်အခကြေးငွေကို ပေးဆောင်ရန် သင်သည် နောက်ထပ် ${currency} လိုအပ်ပါသည်။", "do_not_send": "မပို့ပါနှင့်", - "do_not_send_funds_to_contract_address_warning": "ဒီလိပ်စာကိုရန်ပုံငွေမပို့ပါနဲ့။ ဒီလိပ်စာကိုအမှတ်အသားမပို့ပါနဲ့, ဒီလိပ်စာကိုပို့တဲ့ရန်ပုံငွေမဆိုဆုံးရှုံးသွားမှာပါ။ \n\n မှတ်ချက်။ ။ \n\n မှတ်ချက်။ ။", "do_not_share_warning_text": "ပံ့ပိုးကူညီမှုအပါအဝင် ဤအရာများကို အခြားမည်သူနှင့်မျှ မမျှဝေပါနှင့်။\n\nသင့်ငွေများကို ခိုးယူခံရနိုင်သည်!", "do_not_show_me": "ဒါကို ထပ်မပြနဲ့", "domain_looks_up": "ဒိုမိန်းရှာဖွေမှုများ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 273a65ae5..a7d414b1c 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -222,7 +222,6 @@ "displayable": "Weer te geven", "do_not_have_enough_gas_asset": "U heeft niet genoeg ${currency} om een transactie uit te voeren met de huidige blockchain-netwerkomstandigheden. U heeft meer ${currency} nodig om blockchain-netwerkkosten te betalen, zelfs als u een ander item verzendt.", "do_not_send": "Niet sturen", - "do_not_send_funds_to_contract_address_warning": "Stuur geen fondsen naar dit adres \n\n Dit is slechts een identificatie voor het token, alle fondsen die naar dit adres zijn verzonden, gaan verloren. \n\n Opmerking: cake zou u nooit vragen om een ​​contractadres toe te voegen", "do_not_share_warning_text": "Deel deze met niemand anders, ook niet met support.\n\nUw geld kan en zal worden gestolen!", "do_not_show_me": "laat me dit niet opnieuw zien", "domain_looks_up": "Domein opzoeken", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 046db2187..00eebbe98 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -222,7 +222,6 @@ "displayable": "Wyświetlane", "do_not_have_enough_gas_asset": "Nie masz wystarczającej ilości ${currency}, aby dokonać transakcji przy bieżących warunkach sieci blockchain. Potrzebujesz więcej ${currency}, aby uiścić opłaty za sieć blockchain, nawet jeśli wysyłasz inny zasób.", "do_not_send": "Nie wysyłaj", - "do_not_send_funds_to_contract_address_warning": "Nie wysyłaj środków na ten adres \n\n To jest tylko identyfikator tokena, wszelkie środki wysłane na ten adres zostaną utracone. \n\n", "do_not_share_warning_text": "NIE udostępniaj ich nikomu innemu, w tym pomocy technicznej.\n\nTwoje środki wtedy prawdopodobnie zostaną skradzione!", "do_not_show_me": "Nie pokazuj mi tego ponownie", "domain_looks_up": "Wyszukiwanie domen", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 164cb9530..28bea3e65 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -222,7 +222,6 @@ "displayable": "Exibível", "do_not_have_enough_gas_asset": "Você não tem ${currency} suficiente para fazer uma transação com as condições atuais da rede blockchain. Você precisa de mais ${currency} para pagar as taxas da rede blockchain, mesmo se estiver enviando um ativo diferente.", "do_not_send": "não envie", - "do_not_send_funds_to_contract_address_warning": "Não envie fundos para este endereço \n\n Este é apenas um identificador para o token, quaisquer fundos enviados para este endereço serão perdidos. \n\n NOTA: O bolo nunca solicitaria que você adicione um endereço de contrato", "do_not_share_warning_text": "Não os compartilhe com mais ninguém, incluindo suporte.\n\nSeus fundos podem e serão roubados!", "do_not_show_me": "não me mostre isso novamente", "domain_looks_up": "Pesquisas de domínio", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 28d856191..6ffa87b0f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -222,7 +222,6 @@ "displayable": "Отображаемый", "do_not_have_enough_gas_asset": "У вас недостаточно ${currency} для совершения транзакции при текущих условиях сети блокчейн. Вам нужно больше ${currency} для оплаты комиссий за сеть блокчейна, даже если вы отправляете другой актив.", "do_not_send": "Не отправлять", - "do_not_send_funds_to_contract_address_warning": "Не отправляйте средства на этот адрес \n\n Это просто идентификатор для токена, любые средства, отправленные на этот адрес, будут потеряны. \n\n Примечание: торт никогда не попросит вас добавить адрес контракта", "do_not_share_warning_text": "Не сообщайте их никому, включая техподдержку.\n\nВаши средства могут и будут украдены!", "do_not_show_me": "Не показывай мне это больше", "domain_looks_up": "Поиск доменов", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index ee6d5b7a2..4a41989c8 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -222,7 +222,6 @@ "displayable": "สามารถแสดงได้", "do_not_have_enough_gas_asset": "คุณมี ${currency} ไม่เพียงพอที่จะทำธุรกรรมกับเงื่อนไขเครือข่ายบล็อคเชนในปัจจุบัน คุณต้องมี ${currency} เพิ่มขึ้นเพื่อชำระค่าธรรมเนียมเครือข่ายบล็อคเชน แม้ว่าคุณจะส่งสินทรัพย์อื่นก็ตาม", "do_not_send": "อย่าส่ง", - "do_not_send_funds_to_contract_address_warning": "อย่าส่งเงินไปยังที่อยู่นี้ \n\n นี่เป็นเพียงตัวระบุสำหรับโทเค็นเงินทุนใด ๆ ที่ส่งไปยังที่อยู่นี้จะหายไป \n\n หมายเหตุ: เค้กจะไม่ขอให้คุณเพิ่มที่อยู่สัญญา", "do_not_share_warning_text": "อย่าแชร์ข้อมูลนี้กับใครอื่น รวมถึงฝ่ายสนับสนุนด้วย\n\nการเงินของคุณอาจถูกขโมยโดยไม่หวังดี!", "do_not_show_me": "อย่าแสดงข้อความนี้อีก", "domain_looks_up": "การค้นหาโดเมน", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 2e7b4f4db..f2caa5fcc 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -222,7 +222,6 @@ "displayable": "Maipapakita", "do_not_have_enough_gas_asset": "Wala kang sapat na ${currency} para gumawa ng transaksyon sa kasalukuyang kundisyon ng network ng blockchain. Kailangan mo ng higit pang ${currency} upang magbayad ng mga fee sa network ng blockchain, kahit na nagpapadala ka ng ibang asset.", "do_not_send": "Huwag ipadala", - "do_not_send_funds_to_contract_address_warning": "Huwag magpadala ng pondo sa address na ito\n\n Ito ay isang identifier lamang para sa token, ang anumang pondo na ipinadala sa address na ito ay mawawala.\n\n Tandaan: Ang cake ay hindi hihilingin sa iyo na magdagdag ng isang address ng kontrata", "do_not_share_warning_text": "Huwag ibahagi ang mga ito sa sinuman kasama ang tagatustos.\n\nMaaaring manakaw ang iyong mga pondo!", "do_not_show_me": "Huwag mo itong ipakita muli", "domain_looks_up": "Mga paghahanap ng domain", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index fba4a796e..83545e261 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -222,7 +222,6 @@ "displayable": "Gösterilebilir", "do_not_have_enough_gas_asset": "Mevcut blockchain ağ koşullarıyla işlem yapmak için yeterli ${currency} paranız yok. Farklı bir varlık gönderiyor olsanız bile blockchain ağ ücretlerini ödemek için daha fazla ${currency} miktarına ihtiyacınız var.", "do_not_send": "Gönderme", - "do_not_send_funds_to_contract_address_warning": "Bu adrese fon göndermeyin \n\n Bu sadece jeton için bir tanımlayıcıdır, bu adrese gönderilen fonlar kaybolacaktır. \n\n Not: Kekten asla bir sözleşme adresi eklemenizi istemez", "do_not_share_warning_text": "Bunları destek de dahil olmak üzere başka kimseyle paylaşma.\n\nParan çalınabilir ve çalınacaktır!", "do_not_show_me": "Bana bunu bir daha gösterme", "domain_looks_up": "Etki alanı aramaları", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ff1f2905f..dc6baad6d 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -222,7 +222,6 @@ "displayable": "Відображуваний", "do_not_have_enough_gas_asset": "У вас недостатньо ${currency}, щоб здійснити трансакцію з поточними умовами мережі блокчейн. Вам потрібно більше ${currency}, щоб сплатити комісію мережі блокчейн, навіть якщо ви надсилаєте інший актив.", "do_not_send": "Не надсилайте", - "do_not_send_funds_to_contract_address_warning": "Не надсилайте кошти на цю адресу \n\n Це лише ідентифікатор для маркера, будь -які кошти, надіслані на цю адресу", "do_not_share_warning_text": "Не діліться цим нікому, включно зі службою підтримки.\n\nВаші кошти можуть і будуть вкрадені!", "do_not_show_me": "Не показуй мені це знову", "domain_looks_up": "Пошук доменів", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 8cfe2ec9f..2140accbe 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -222,7 +222,6 @@ "displayable": "قابل نمائش", "do_not_have_enough_gas_asset": "آپ کے پاس موجودہ بلاکچین نیٹ ورک کی شرائط کے ساتھ لین دین کرنے کے لیے کافی ${currency} نہیں ہے۔ آپ کو بلاکچین نیٹ ورک کی فیس ادا کرنے کے لیے مزید ${currency} کی ضرورت ہے، چاہے آپ کوئی مختلف اثاثہ بھیج رہے ہوں۔", "do_not_send": "مت بھیجیں۔", - "do_not_send_funds_to_contract_address_warning": "اس پتے پر فنڈز نہ بھیجیں \n\n یہ ٹوکن کے لئے صرف ایک شناخت کنندہ ہے ، اس پتے پر بھیجے گئے کوئی بھی فنڈز ضائع ہوجائیں گے۔ \n\n نوٹ: کیک آپ کو کبھی بھی معاہدہ کا پتہ شامل کرنے کے لئے نہیں کہے گا۔", "do_not_share_warning_text": "سپورٹ سمیت کسی اور کے ساتھ ان کا اشتراک نہ کریں۔\\n\\nآپ کے فنڈز چوری ہو سکتے ہیں اور ہو جائیں گے!", "do_not_show_me": "مجھے یہ دوبارہ مت دکھانا", "domain_looks_up": "ڈومین تلاش کرنا", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 444c1d6d8..a21da9e68 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -223,7 +223,6 @@ "displayable": "Có thể hiển thị", "do_not_have_enough_gas_asset": "Bạn không có đủ ${currency} để thực hiện giao dịch với điều kiện mạng blockchain hiện tại. Bạn cần thêm ${currency} để trả phí mạng blockchain, ngay cả khi bạn đang gửi tài sản khác.", "do_not_send": "Đừng gửi", - "do_not_send_funds_to_contract_address_warning": "Không gửi tiền đến địa chỉ này\n\n Đây chỉ là một mã định danh cho mã thông báo, bất kỳ khoản tiền nào được gửi đến địa chỉ này sẽ bị mất.\n\n Lưu ý: Bánh sẽ không bao giờ yêu cầu bạn thêm địa chỉ hợp đồng", "do_not_share_warning_text": "Không chia sẻ điều này với bất kỳ ai, bao gồm cả bộ phận hỗ trợ.\n\nTài sản của bạn có thể và sẽ bị đánh cắp!", "do_not_show_me": "Không hiển thị lại cho tôi", "domain_looks_up": "Tra cứu tên miền", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 15b90b973..8a30c5e08 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -222,7 +222,6 @@ "displayable": "A lè ṣàfihàn ẹ̀", "do_not_have_enough_gas_asset": "O ko ni to ${currency} lati ṣe idunadura kan pẹlu awọn ipo nẹtiwọki blockchain lọwọlọwọ. O nilo diẹ sii ${currency} lati san awọn owo nẹtiwọọki blockchain, paapaa ti o ba nfi dukia miiran ranṣẹ.", "do_not_send": "Ẹ kò ránṣ", - "do_not_send_funds_to_contract_address_warning": "Maṣe fi owo ranṣẹ si adirẹsi yii \n\n Eyi jẹ idamọ fun àmi, eyikeyi awọn owo ti a firanṣẹ si adirẹsi yii yoo ko beere lọwọ rẹ lati ṣafikun adirẹsi adehun kan", "do_not_share_warning_text": "Ẹ kò pín wọnyìí sí ẹnikẹ́ni. Ẹ sì kò pin wọnyìí sí ìranlọ́wọ́. Ẹnikẹ́ni lè jí owó yín! Wọ́n máa jí owó yín!", "do_not_show_me": "Kò fi eléyìí hàn mi mọ́", "domain_looks_up": "Awọn wiwa agbegbe", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index ae7e15132..6a3b9d4c8 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -222,7 +222,6 @@ "displayable": "可显示", "do_not_have_enough_gas_asset": "您没有足够的 ${currency} 来在当前的区块链网络条件下进行交易。即使您发送的是不同的资产,您也需要更多的 ${currency} 来支付区块链网络费用。", "do_not_send": "不要发送", - "do_not_send_funds_to_contract_address_warning": "请勿将资金发送到此地址\n\n这只是令牌的标识符,任何发送到此地址的资金都将丢失。\n\n注意:蛋糕永远不会要求您添加合同地址", "do_not_share_warning_text": "请勿与其他任何人分享这些信息,包括支持人员。\n\n您的资金可能而且将会被盗!", "do_not_show_me": "不再提示", "domain_looks_up": "域名查找", From e9b5b184283a880d10878994c692583636a1fceb Mon Sep 17 00:00:00 2001 From: Rafael Date: Thu, 26 Sep 2024 01:49:09 +0000 Subject: [PATCH 093/142] Fix regex pattern (#1695) * fix: regex pattern * fix: extracting regex --- lib/core/address_validator.dart | 117 +++++++++++++++++--------------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index e76cb99d1..06a30a2c3 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -5,6 +5,9 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; +const BEFORE_REGEX = '(^|\\s)'; +const AFTER_REGEX = '(\$|\\s)'; + class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) : super( @@ -19,21 +22,23 @@ class AddressValidator extends TextValidator { length: getLength(type)); static String getPattern(CryptoCurrency type) { + var pattern = ""; if (type is Erc20Token) { - return '0x[0-9a-zA-Z]'; + pattern = '0x[0-9a-zA-Z]+'; } switch (type) { case CryptoCurrency.xmr: - return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; + pattern = '4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}'; case CryptoCurrency.ada: - return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' - '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; + pattern = '[0-9a-zA-Z]{59}|[0-9a-zA-Z]{92}|[0-9a-zA-Z]{104}' + '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; case CryptoCurrency.btc: - return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${SilentPaymentAddress.regex.pattern}\$'; + pattern = + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.nano: - return '[0-9a-zA-Z_]'; + pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.banano: - return '[0-9a-zA-Z_]'; + pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.usdc: case CryptoCurrency.usdcpoly: case CryptoCurrency.usdtPoly: @@ -69,11 +74,11 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dydx: case CryptoCurrency.steth: case CryptoCurrency.shib: - return '0x[0-9a-zA-Z]'; + pattern = '0x[0-9a-zA-Z]+'; case CryptoCurrency.xrp: - return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; + pattern = '[0-9a-zA-Z]{34}|X[0-9a-zA-Z]{46}'; case CryptoCurrency.xhv: - return '^hvx|hvi|hvs[0-9a-zA-Z]'; + pattern = 'hvx|hvi|hvs[0-9a-zA-Z]+'; case CryptoCurrency.xag: case CryptoCurrency.xau: case CryptoCurrency.xaud: @@ -95,40 +100,43 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dash: case CryptoCurrency.eos: case CryptoCurrency.wow: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]+'; case CryptoCurrency.bch: - return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{42}\$|^bitcoincash:q|p[0-9a-zA-Z]{41}\$|^bitcoincash:q|p[0-9a-zA-Z]{42}\$'; + pattern = + '(?!bitcoincash:)[0-9a-zA-Z]*|(?!bitcoincash:)q|p[0-9a-zA-Z]{41}|(?!bitcoincash:)q|p[0-9a-zA-Z]{42}|bitcoincash:q|p[0-9a-zA-Z]{41}|bitcoincash:q|p[0-9a-zA-Z]{42}'; case CryptoCurrency.bnb: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]+'; case CryptoCurrency.ltc: - return '^(?!(ltc|LTC)1)[0-9a-zA-Z]*\$|(^LTC1[A-Z0-9]*\$)|(^ltc1[a-z0-9]*\$)'; + pattern = '((?!(ltc|LTC)1)[0-9a-zA-Z]*)|(LTC1[A-Z0-9]*)|(ltc1[a-z0-9]*)'; case CryptoCurrency.hbar: - return '[0-9a-zA-Z.]'; + pattern = '[0-9a-zA-Z.]+'; case CryptoCurrency.zaddr: - return '^zs[0-9a-zA-Z]{75}'; + pattern = 'zs[0-9a-zA-Z]{75}'; case CryptoCurrency.zec: - return '^t1[0-9a-zA-Z]{33}\$|^t3[0-9a-zA-Z]{33}\$'; + pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; case CryptoCurrency.dcr: - return 'D[ksecS]([0-9a-zA-Z])+'; + pattern = 'D[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: - return '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; + pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.near: - return '[0-9a-f]{64}'; + pattern = '[0-9a-f]{64}'; case CryptoCurrency.rune: - return 'thor1[0-9a-z]{38}'; + pattern = 'thor1[0-9a-z]{38}'; case CryptoCurrency.scrt: - return 'secret1[0-9a-z]{38}'; + pattern = 'secret1[0-9a-z]{38}'; case CryptoCurrency.stx: - return 'S[MP][0-9a-zA-Z]+'; + pattern = 'S[MP][0-9a-zA-Z]+'; case CryptoCurrency.kmd: - return 'R[0-9a-zA-Z]{33}'; + pattern = 'R[0-9a-zA-Z]{33}'; case CryptoCurrency.pivx: - return 'D([1-9a-km-zA-HJ-NP-Z]){33}'; + pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.btcln: - return '^(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; + pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; default: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]+'; } + + return '$BEFORE_REGEX($pattern)$AFTER_REGEX'; } static List? getLength(CryptoCurrency type) { @@ -269,56 +277,53 @@ class AddressValidator extends TextValidator { } static String? getAddressFromStringPattern(CryptoCurrency type) { + String? pattern = null; + switch (type) { case CryptoCurrency.xmr: case CryptoCurrency.wow: - return '([^0-9a-zA-Z]|^)4[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; + pattern = '(4[0-9a-zA-Z]{94})' + '|(8[0-9a-zA-Z]{94})' + '|([0-9a-zA-Z]{106})'; case CryptoCurrency.btc: - return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type - '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)' //P2trAddress type - '|${SilentPaymentAddress.regex.pattern}\$'; - + pattern = + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: - return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; + pattern = '(L[a-zA-Z0-9]{26,33})' + '|([LM][a-km-zA-HJ-NP-Z1-9]{26,33})' + '|(ltc[a-zA-Z0-9]{26,45})'; case CryptoCurrency.eth: - return '0x[0-9a-zA-Z]{42}'; case CryptoCurrency.maticpoly: - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]+'; case CryptoCurrency.nano: - return 'nano_[0-9a-zA-Z]{60}'; + pattern = 'nano_[0-9a-zA-Z]{60}'; case CryptoCurrency.banano: - return 'ban_[0-9a-zA-Z]{60}'; + pattern = 'ban_[0-9a-zA-Z]{60}'; case CryptoCurrency.bch: - return 'bitcoincash:q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' - '|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'; + pattern = '(bitcoincash:)?q[0-9a-zA-Z]{41,42}'; case CryptoCurrency.sol: - return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + pattern = '[1-9A-HJ-NP-Za-km-z]+'; case CryptoCurrency.trx: - return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; default: if (type.tag == CryptoCurrency.eth.title) { - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.maticpoly.tag) { - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.sol.title) { - return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}'; } if (type.tag == CryptoCurrency.trx.title) { - return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; } - - return null; } + + if (pattern != null) { + return "$BEFORE_REGEX($pattern)$AFTER_REGEX"; + } + + return null; } } From 11584844d55892f31cedf303a919c1991a93bd3e Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Thu, 26 Sep 2024 04:51:38 +0300 Subject: [PATCH 094/142] Verify seed phrase (#1700) * add confirm passphrase field and show/hide passphrase feature * change error message allow force appending translation to overwrite * minor fix for showing frozen balance correctly [skip ci] * remove debug prints [skip ci] --- cw_bitcoin/lib/electrum_transaction_info.dart | 2 +- cw_bitcoin/lib/electrum_wallet.dart | 12 +++ cw_bitcoin/pubspec.lock | 20 ++--- .../advanced_privacy_settings_page.dart | 76 +++++++++++++------ .../wallet_restore_from_seed_form.dart | 13 +++- .../dashboard/home_settings_view_model.dart | 14 +--- res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 4 +- res/values/strings_en.arb | 4 +- res/values/strings_es.arb | 2 + res/values/strings_fr.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_hy.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_vi.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + tool/append_translation.dart | 9 ++- tool/utils/translation/arb_file_utils.dart | 4 +- 36 files changed, 155 insertions(+), 55 deletions(-) diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index f2ef251c7..1ab7799e3 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -35,7 +35,7 @@ class ElectrumTransactionInfo extends TransactionInfo { List? outputAddresses, required TransactionDirection direction, required bool isPending, - required bool isReplaced, + bool isReplaced = false, required DateTime date, required int confirmations, String? to, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index fee115650..71e537186 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1902,6 +1902,18 @@ abstract class ElectrumWalletBase var totalConfirmed = 0; var totalUnconfirmed = 0; + unspentCoinsInfo.values.forEach((info) { + unspentCoins.forEach((element) { + if (element.hash == info.hash && + element.vout == info.vout && + info.isFrozen && + element.bitcoinAddressRecord.address == info.address && + element.value == info.value) { + totalFrozen += element.value; + } + }); + }); + if (hasSilentPaymentsScanning) { // Add values from unspent coins that are not fetched by the address list // i.e. scanned silent payments diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index a2478af3c..c46260cee 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.5.5" async: dependency: transitive description: @@ -71,7 +71,7 @@ packages: description: path: "." ref: cake-update-v7 - resolved-ref: bc49e3b1cba601828f8ddc3d016188d8c2499088 + resolved-ref: f577e83fe78766b2655ea0602baa9299b953a31b url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" @@ -296,10 +296,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" ffigen: dependency: transitive description: @@ -553,10 +553,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mobx: dependency: "direct main" description: @@ -713,10 +713,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" reactive_ble_mobile: dependency: transitive description: @@ -855,7 +855,7 @@ packages: description: path: "." ref: "sp_v4.0.0" - resolved-ref: "9b04f4b0af80dd7dae9274b496a53c23dcc80ea5" + resolved-ref: ca1add293bd1e06920aa049b655832da50d0dab2 url: "https://github.com/cake-tech/sp_scanner" source: git version: "0.0.1" 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 078768a7a..8b7be18a1 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -85,7 +85,9 @@ class _AdvancedPrivacySettingsBody extends StatefulWidget { class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBody> { final TextEditingController passphraseController = TextEditingController(); + final TextEditingController confirmPassphraseController = TextEditingController(); final _formKey = GlobalKey(); + final _passphraseFormKey = GlobalKey(); bool? testnetValue; bool obscurePassphrase = true; @@ -93,9 +95,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo @override void initState() { passphraseController.text = widget.seedTypeViewModel.passphrase ?? ''; - - passphraseController - .addListener(() => widget.seedTypeViewModel.setPassphrase(passphraseController.text)); + confirmPassphraseController.text = widget.seedTypeViewModel.passphrase ?? ''; if (widget.isChildWallet) { if (widget.privacySettingsViewModel.type == WalletType.bitcoin) { @@ -205,18 +205,47 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo if (widget.privacySettingsViewModel.hasPassphraseOption) Padding( padding: EdgeInsets.all(24), - child: BaseTextFormField( - hintText: S.current.passphrase, - controller: passphraseController, - obscureText: obscurePassphrase, - suffixIcon: GestureDetector( - onTap: () => setState(() { - obscurePassphrase = !obscurePassphrase; - }), - child: Icon( - Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, - ), + child: Form( + key: _passphraseFormKey, + child: Column( + children: [ + BaseTextFormField( + hintText: S.of(context).passphrase, + controller: passphraseController, + obscureText: obscurePassphrase, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, + ), + ), + ), + const SizedBox(height: 10), + BaseTextFormField( + hintText: S.of(context).confirm_passphrase, + controller: confirmPassphraseController, + obscureText: obscurePassphrase, + validator: (text) { + if (text == passphraseController.text) { + return null; + } + + return S.of(context).passphrases_doesnt_match; + }, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, + ), + ), + ), + ], ), ), ), @@ -272,7 +301,8 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo } widget.nodeViewModel.save(); - } else if (testnetValue == true) { + } + if (testnetValue == true) { // TODO: add type (mainnet/testnet) to Node class so when switching wallets the node can be switched to a matching type // Currently this is so you can create a working testnet wallet but you need to keep switching back the node if you use multiple wallets at once widget.nodeViewModel.address = publicBitcoinTestnetElectrumAddress; @@ -280,6 +310,13 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo widget.nodeViewModel.save(); } + if (passphraseController.text.isNotEmpty) { + if (_passphraseFormKey.currentState != null && !_passphraseFormKey.currentState!.validate()) { + return; + } + + widget.seedTypeViewModel.setPassphrase(passphraseController.text); + } Navigator.pop(context); }, @@ -318,11 +355,4 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ); }); } - - @override - void dispose() { - passphraseController - .removeListener(() => widget.seedTypeViewModel.setPassphrase(passphraseController.text)); - super.dispose(); - } } 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 67576144c..897a6bed0 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -78,6 +78,8 @@ class WalletRestoreFromSeedFormState extends State { void Function()? repeatedPasswordListener; void Function()? passphraseListener; + bool obscurePassphrase = true; + @override void initState() { _setSeedType(widget.seedSettingsViewModel.moneroSeedType); @@ -283,7 +285,16 @@ class WalletRestoreFromSeedFormState extends State { BaseTextFormField( hintText: S.current.passphrase, controller: passphraseController, - obscureText: true, + obscureText: obscurePassphrase, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, + ), + ), ), ] ])); diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 4269078a5..1aeacd28f 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -151,34 +151,24 @@ abstract class HomeSettingsViewModelBase with Store { bool isEthereum = _balanceViewModel.wallet.type == WalletType.ethereum; - print('An extra log for now'); - bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis( contractAddress, isEthereum ? 'eth' : 'polygon', ); - print('Is Potential Scam from Moralis: $isPotentialScamViaMoralis'); - bool isPotentialScamViaExplorers = await _isPotentialScamTokenViaExplorers( contractAddress, isEthereum: isEthereum, ); - print('Is Potential Scam from Explorers: $isPotentialScamViaExplorers'); - bool isUnverifiedContract = await _isContractUnverified( contractAddress, isEthereum: isEthereum, ); - print('Is Unverified Contract: $isUnverifiedContract'); - final showWarningForContractAddress = isPotentialScamViaMoralis || isUnverifiedContract || isPotentialScamViaExplorers; - print('Show Warning: $showWarningForContractAddress'); - return showWarningForContractAddress; } finally { isValidatingContractAddress = false; @@ -272,8 +262,8 @@ abstract class HomeSettingsViewModelBase with Store { final decodedResponse = jsonDecode(response.body) as Map; if (decodedResponse['status'] != '1') { - print('${response.body}\n'); - print('${decodedResponse['result']}\n'); + log('${response.body}\n'); + log('${decodedResponse['result']}\n'); return true; } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index c9a0c1a37..48d744f08 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "سيؤدي هذا الإجراء إلى حذف هذه المحفظة. هل ترغب في الاستمرار؟", "confirm_fee_deduction": "تأكيد خصم الرسوم", "confirm_fee_deduction_content": "هل توافق على خصم الرسوم من الإخراج؟", + "confirm_passphrase": "تأكيد عبارة المرور", "confirm_sending": "تأكيد الإرسال", "confirm_silent_payments_switch_node": "العقدة الحالية لا تدعم المدفوعات الصامتة \\ ncake wallet سوف تتحول إلى عقدة متوافقة ، فقط للمسح الضوئي", "confirmations": "التأكيدات", @@ -451,6 +452,7 @@ "overwrite_amount": "تغير المبلغ", "pairingInvalidEvent": "ﺢﻟﺎﺻ ﺮﻴﻏ ﺙﺪﺣ ﻥﺍﺮﻗﺇ", "passphrase": "عبارة الممر (اختياري)", + "passphrases_doesnt_match": "لا تتطابق عبارات المرور ، يرجى المحاولة مرة أخرى", "password": "كلمة المرور", "paste": "لصق", "pause_wallet_creation": ".ﺎﻴًﻟﺎﺣ ﺎﺘًﻗﺆﻣ ﺔﻔﻗﻮﺘﻣ Haven Wallet ءﺎﺸﻧﺇ ﻰﻠﻋ ﺓﺭﺪﻘﻟﺍ", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 2021dd220..14cce0cfb 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Този портфейл ще бъде изтрит. Искате ли да продължите?", "confirm_fee_deduction": "Потвърдете приспадането на таксите", "confirm_fee_deduction_content": "Съгласни ли сте да приспадате таксата от продукцията?", + "confirm_passphrase": "Потвърдете парола", "confirm_sending": "Потвърждаване на изпращането", "confirm_silent_payments_switch_node": "Текущият ви възел не поддържа Silent Payments \\ Ncake Wallet ще премине към съвместим възел, само за сканиране", "confirmations": "потвърждения", @@ -451,6 +452,7 @@ "overwrite_amount": "Промени сума", "pairingInvalidEvent": "Невалидно събитие при сдвояване", "passphrase": "Passphrase (по избор)", + "passphrases_doesnt_match": "Пасифрази не съвпадат, моля, опитайте отново", "password": "Парола", "paste": "Поставяне", "pause_wallet_creation": "Възможността за създаване на Haven Wallet в момента е на пауза.", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index bc8a8903a..344c5bf3b 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Tato akce smaže tuto peněženku. Přejete si pokračovat?", "confirm_fee_deduction": "Potvrďte odpočet poplatků", "confirm_fee_deduction_content": "Souhlasíte s odečtením poplatku z výstupu?", + "confirm_passphrase": "Potvrďte přístupovou frázi", "confirm_sending": "Potvrdit odeslání", "confirm_silent_payments_switch_node": "Váš aktuální uzel nepodporuje tiché platby \\ Ncake peněženka se přepne na kompatibilní uzel, pouze pro skenování", "confirmations": "Potvrzení", @@ -451,6 +452,7 @@ "overwrite_amount": "Přepsat částku", "pairingInvalidEvent": "Neplatná událost párování", "passphrase": "Passphrase (volitelné)", + "passphrases_doesnt_match": "Passfrázy se neshodují, zkuste to znovu", "password": "Heslo", "paste": "Vložit", "pause_wallet_creation": "Možnost vytvářet Haven Wallet je momentálně pozastavena.", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 4a7565d0c..6eb6ea3c3 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Diese Aktion löscht diese Wallet. Möchten Sie fortfahren?", "confirm_fee_deduction": "Gebührenabzug bestätigen", "confirm_fee_deduction_content": "Stimmen Sie zu, die Gebühr von der Ausgabe abzuziehen?", + "confirm_passphrase": "Passphrase bestätigen", "confirm_sending": "Senden bestätigen", "confirm_silent_payments_switch_node": "Ihr aktueller Knoten unterstützt keine stillen Zahlungen \\ NCAKE Wallet wechselt zu einem kompatiblen Knoten, nur zum Scannen", "confirmations": "Bestätigungen", @@ -451,6 +452,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Paarung ungültiges Ereignis", "passphrase": "Passphrase (optional)", + "passphrases_doesnt_match": "Passphrasen stimmen nicht überein, bitte versuchen Sie es erneut", "password": "Passwort", "paste": "Einfügen", "pause_wallet_creation": "Die Möglichkeit, Haven Wallet zu erstellen, ist derzeit pausiert.", @@ -465,8 +467,8 @@ "placeholder_transactions": "Ihre Transaktionen werden hier angezeigt", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist", "please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.", - "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.", + "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.", "please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 975667036..c95816b28 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "This action will delete this wallet. Do you wish to continue?", "confirm_fee_deduction": "Confirm Fee Deduction", "confirm_fee_deduction_content": "Do you agree to deduct the fee from the output?", + "confirm_passphrase": "Confirm passphrase", "confirm_sending": "Confirm sending", "confirm_silent_payments_switch_node": "Your current node does not support silent payments\\nCake Wallet will switch to a compatible node, just for scanning", "confirmations": "Confirmations", @@ -297,8 +298,8 @@ "failed_authentication": "Failed authentication. ${state_error}", "faq": "FAQ", "features": "Features", - "fee_rate": "Fee rate", "fee_less_than_min": "Selected Fee is less than the minimum, please increase the fees to be able to send the transaction", + "fee_rate": "Fee rate", "fetching": "Fetching", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -451,6 +452,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (Optional)", + "passphrases_doesnt_match": "Passphrases do not match, please try again", "password": "Password", "paste": "Paste", "pause_wallet_creation": "Ability to create Haven Wallet is currently paused.", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 19356fc20..57cadbd08 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Desea continuar?", "confirm_fee_deduction": "Confirmar la deducción de la tarifa", "confirm_fee_deduction_content": "¿Acepta deducir la tarifa de la producción?", + "confirm_passphrase": "Confirmar la frase de pases", "confirm_sending": "Confirmar envío", "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ ncake billet cambiará a un nodo compatible, solo para escanear", "confirmations": "Confirmaciones", @@ -451,6 +452,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Evento de emparejamiento no válido", "passphrase": "Passfrase (opcional)", + "passphrases_doesnt_match": "Las frases de contrato no coinciden, intente nuevamente", "password": "Contraseña", "paste": "Pegar", "pause_wallet_creation": "La capacidad para crear Haven Wallet está actualmente pausada.", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 4b57d026b..2de3c6d03 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Cette action va supprimer ce portefeuille (wallet). Souhaitez-vous contnuer ?", "confirm_fee_deduction": "Confirmer la déduction des frais", "confirm_fee_deduction_content": "Acceptez-vous de déduire les frais de la production?", + "confirm_passphrase": "Confirmer la phrase passante", "confirm_sending": "Confirmer l'envoi", "confirm_silent_payments_switch_node": "Votre nœud actuel ne prend pas en charge les paiements silencieux \\ ncake qui passera à un nœud compatible, juste pour la numérisation", "confirmations": "Confirmations", @@ -451,6 +452,7 @@ "overwrite_amount": "Remplacer le montant", "pairingInvalidEvent": "Événement de couplage non valide", "passphrase": "Phrase de passe (facultative)", + "passphrases_doesnt_match": "Les phrases de passe ne correspondent pas, veuillez réessayer", "password": "Mot de passe", "paste": "Coller", "pause_wallet_creation": "La possibilité de créer Haven Wallet est actuellement suspendue.", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 34cbf0a69..3adefbea0 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Wannan aikin zai share wannan walat. Kuna so ku ci gaba?", "confirm_fee_deduction": "Tabbatar da cire kudade", "confirm_fee_deduction_content": "Shin kun yarda ku cire kuɗin daga fitarwa?", + "confirm_passphrase": "Tabbatar da kalmar wucewa", "confirm_sending": "Tabbatar da aikawa", "confirm_silent_payments_switch_node": "Kumburinku na yanzu ba ya goyan bayan biyan shiru da shiru \\ NCADA Wallet zai canza zuwa kumburi mai dacewa, don bincika", "confirmations": "Tabbatar", @@ -453,6 +454,7 @@ "overwrite_amount": "Rubuta adadin", "pairingInvalidEvent": "Haɗa Lamarin mara inganci", "passphrase": "Passphrase (Zabi)", + "passphrases_doesnt_match": "Passphrases bai dace ba, don Allah sake gwadawa", "password": "Kalmar wucewa", "paste": "Manna", "pause_wallet_creation": "A halin yanzu an dakatar da ikon ƙirƙirar Haven Wallet.", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index e321358ee..403acec3e 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "यह क्रिया इस वॉलेट को हटा देगी। क्या आप जारी रखना चाहते हैं?", "confirm_fee_deduction": "शुल्क कटौती की पुष्टि करें", "confirm_fee_deduction_content": "क्या आप आउटपुट से शुल्क में कटौती करने के लिए सहमत हैं?", + "confirm_passphrase": "पासफ़्रेज़ की पुष्टि करें", "confirm_sending": "भेजने की पुष्टि करें", "confirm_silent_payments_switch_node": "आपका वर्तमान नोड मूक भुगतान का समर्थन नहीं करता है \\ ncake वॉलेट एक संगत नोड पर स्विच करेगा, बस स्कैनिंग के लिए", "confirmations": "पुष्टिकरण", @@ -451,6 +452,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "अमान्य ईवेंट युग्मित करना", "passphrase": "पासफ्रेज़ (वैकल्पिक)", + "passphrases_doesnt_match": "PassPhrases मेल नहीं खाता, कृपया पुनः प्रयास करें", "password": "पारण शब्द", "paste": "पेस्ट करें", "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index cd91d5291..e225a11b8 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Ovom ćete radnjom izbrisati ovaj novčanik. Želite li nastaviti?", "confirm_fee_deduction": "Potvrdite odbitak naknade", "confirm_fee_deduction_content": "Slažete li se da ćete odbiti naknadu od izlaza?", + "confirm_passphrase": "Potvrdite prolaznu frazu", "confirm_sending": "Potvrdi slanje", "confirm_silent_payments_switch_node": "Vaš trenutni čvor ne podržava tiha plaćanja \\ ncake novčanik prebacit će se na kompatibilni čvor, samo za skeniranje", "confirmations": "Potvrde", @@ -451,6 +452,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Nevažeći događaj uparivanja", "passphrase": "Prolaznica (neobavezno)", + "passphrases_doesnt_match": "Prolazne fraze se ne podudaraju, pokušajte ponovo", "password": "Lozinka", "paste": "Zalijepi", "pause_wallet_creation": "Mogućnost stvaranja novčanika Haven trenutno je pauzirana.", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 8c65817c2..addddb34c 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Այս գործողությունը կջնջի այս դրամապանակը: Ցանկանու՞մ եք շարունակել։", "confirm_fee_deduction": "Հաստատեք միջնորդավճար հանումը", "confirm_fee_deduction_content": "Դուք համաձայն եք միջնորդավճար հանել արդյունքից?", + "confirm_passphrase": "Հաստատեք գաղտնաբառը", "confirm_sending": "Հաստատեք ուղարկումը", "confirm_silent_payments_switch_node": "Ձեր ընթացիկ հանգույցը չի աջակցում Լուռ վճարումներին\nCake Wallet-ը կանցնի համատեղելի հանգույց, միայն սկանավորման համար", "confirmations": "Հաստատումներ", @@ -451,6 +452,7 @@ "overwrite_amount": "Գրեք գումարը", "pairingInvalidEvent": "Սխալ միացում", "passphrase": "Պարող արտահայտություն (Ոչ պարտադիր)", + "passphrases_doesnt_match": "Անհատները չեն համընկնում, խնդրում ենք կրկին փորձել", "password": "Գաղտնաբառ", "paste": "Տեղադրել", "pause_wallet_creation": "Հնարավորություն ստեղծել Haven Դրամապանակ ընթացիկ դադարեցված է", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index a888a336a..7848ab0ab 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Tindakan ini akan menghapus dompet ini. Apakah Anda ingin melanjutkan?", "confirm_fee_deduction": "Konfirmasi pengurangan biaya", "confirm_fee_deduction_content": "Apakah Anda setuju untuk mengurangi biaya dari output?", + "confirm_passphrase": "Konfirmasi frasa sandi", "confirm_sending": "Konfirmasi pengiriman", "confirm_silent_payments_switch_node": "Node Anda saat ini tidak mendukung pembayaran diam \\ ncake Wallet akan beralih ke simpul yang kompatibel, hanya untuk pemindaian", "confirmations": "Konfirmasi", @@ -453,6 +454,7 @@ "overwrite_amount": "Timpa jumlah", "pairingInvalidEvent": "Menyandingkan Acara Tidak Valid", "passphrase": "Frasa sandi (opsional)", + "passphrases_doesnt_match": "Sandi tidak cocok, coba lagi", "password": "Kata Sandi", "paste": "Tempel", "pause_wallet_creation": "Kemampuan untuk membuat Haven Wallet saat ini dijeda.", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 0e307ac9f..6f40f0633 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Questa azione cancellerà questo portafoglio. Desideri continuare?", "confirm_fee_deduction": "Conferma la detrazione delle commissioni", "confirm_fee_deduction_content": "Accetti di detrarre la commissione dall'output?", + "confirm_passphrase": "Conferma passphrase", "confirm_sending": "Conferma l'invio", "confirm_silent_payments_switch_node": "Il tuo nodo corrente non supporta i pagamenti silenziosi \\ ncake Wallet passerà a un nodo compatibile, solo per la scansione", "confirmations": "Conferme", @@ -453,6 +454,7 @@ "overwrite_amount": "Sovrascrivi quantità", "pairingInvalidEvent": "Associazione evento non valido", "passphrase": "Passphrase (opzionale)", + "passphrases_doesnt_match": "Le passphrasi non corrispondono, riprova", "password": "Password", "paste": "Incolla", "pause_wallet_creation": "La possibilità di creare Haven Wallet è attualmente sospesa.", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index efb7bcc81..a850b78c6 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "このアクションにより、このウォレットが削除されます。 続行しますか?", "confirm_fee_deduction": "料金控除を確認します", "confirm_fee_deduction_content": "出力から料金を差し引くことに同意しますか?", + "confirm_passphrase": "パスフレーズを確認します", "confirm_sending": "送信を確認", "confirm_silent_payments_switch_node": "現在のノードはサイレントペイメントをサポートしていません\\ ncakeウォレットは、スキャン用に互換性のあるノードに切り替えます", "confirmations": "確認", @@ -452,6 +453,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "ペアリング無効イベント", "passphrase": "パスフレーズ(オプション)", + "passphrases_doesnt_match": "パスフレーズは一致しません。もう一度やり直してください", "password": "パスワード", "paste": "ペースト", "pause_wallet_creation": "Haven Wallet を作成する機能は現在一時停止されています。", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 24b6acc8c..86725aa5c 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "이 작업은이 지갑을 삭제합니다. 계속 하시겠습니까?", "confirm_fee_deduction": "수수료 공제를 확인하십시오", "confirm_fee_deduction_content": "출력에서 수수료를 공제하는 데 동의하십니까?", + "confirm_passphrase": "암호를 확인하십시오", "confirm_sending": "전송 확인", "confirm_silent_payments_switch_node": "현재 노드는 무음 지불을 지원하지 않습니다 \\ ncake 지갑은 스캔을 위해 호환 가능한 노드로 전환됩니다.", "confirmations": "확인", @@ -451,6 +452,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "잘못된 이벤트 페어링", "passphrase": "암호화 (선택 사항)", + "passphrases_doesnt_match": "패스 프레이즈가 일치하지 않습니다. 다시 시도하십시오", "password": "암호", "paste": "풀", "pause_wallet_creation": "Haven Wallet 생성 기능이 현재 일시 중지되었습니다.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index a2d783e52..588f8bf4e 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "ဤလုပ်ဆောင်ချက်သည် ဤပိုက်ဆံအိတ်ကို ဖျက်လိုက်ပါမည်။ ဆက်လုပ်လိုပါသလား။", "confirm_fee_deduction": "အခကြေးငွေကိုနှုတ်ယူခြင်း", "confirm_fee_deduction_content": "output မှအခကြေးငွေကိုယူရန်သဘောတူပါသလား။", + "confirm_passphrase": "passphrase အတည်ပြုပါ", "confirm_sending": "ပေးပို့အတည်ပြုပါ။", "confirm_silent_payments_switch_node": "သင်၏လက်ရှိ node သည်အသံတိတ်ငွေပေးချေမှုကိုမပံ့ပိုးပါဟု \\ t", "confirmations": "အတည်ပြုချက်များ", @@ -451,6 +452,7 @@ "overwrite_amount": "ပမာဏကို ထပ်ရေးပါ။", "pairingInvalidEvent": "မမှန်ကန်သောဖြစ်ရပ်ကို တွဲချိတ်ခြင်း။", "passphrase": "passphrase (optional)", + "passphrases_doesnt_match": "passphrases မကိုက်ညီဘူး, ကျေးဇူးပြုပြီးထပ်ကြိုးစားပါ", "password": "စကားဝှက်", "paste": "ငါးပိ", "pause_wallet_creation": "Haven Wallet ဖန်တီးနိုင်မှုကို လောလောဆယ် ခေတ္တရပ်ထားသည်။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index a7d414b1c..38bf2d69f 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Met deze actie wordt deze portemonnee verwijderd. Wilt u doorgaan?", "confirm_fee_deduction": "Bevestig de aftrek van de kosten", "confirm_fee_deduction_content": "Stemt u ermee in om de vergoeding af te trekken van de output?", + "confirm_passphrase": "Bevestig Passaspherase", "confirm_sending": "Bevestig verzending", "confirm_silent_payments_switch_node": "Uw huidige knooppunt ondersteunt geen stille betalingen \\ ncake -portemonnee schakelt over naar een compatibele knoop", "confirmations": "Bevestigingen", @@ -451,6 +452,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Koppelen Ongeldige gebeurtenis", "passphrase": "PassaspHRASE (optioneel)", + "passphrases_doesnt_match": "Passaspelfiaal komt niet overeen, probeer het opnieuw", "password": "Wachtwoord", "paste": "Plakken", "pause_wallet_creation": "De mogelijkheid om Haven Wallet te maken is momenteel onderbroken.", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 00eebbe98..fc776a1af 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Ta czynność usunie ten portfel. Czy chcesz kontynuować?", "confirm_fee_deduction": "Potwierdź odliczenie opłaty", "confirm_fee_deduction_content": "Czy zgadzasz się odliczyć opłatę od wyników?", + "confirm_passphrase": "Potwierdź hasło", "confirm_sending": "Potwierdź wysłanie", "confirm_silent_payments_switch_node": "Twój obecny węzeł nie obsługuje cichych płatności \\ NCAKE Portfel przełączy się na kompatybilny węzeł, tylko do skanowania", "confirmations": "Potwierdzenia", @@ -451,6 +452,7 @@ "overwrite_amount": "Nadpisz ilość", "pairingInvalidEvent": "Nieprawidłowe zdarzenie parowania", "passphrase": "PassPhraza (opcjonalnie)", + "passphrases_doesnt_match": "Passfrazy nie pasują, spróbuj ponownie", "password": "Hasło", "paste": "Wklej", "pause_wallet_creation": "Możliwość utworzenia Portfela Haven jest obecnie wstrzymana.", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 28bea3e65..6a892cda0 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Esta ação excluirá esta carteira. Você deseja continuar?", "confirm_fee_deduction": "Confirme dedução da taxa", "confirm_fee_deduction_content": "Você concorda em deduzir a taxa da saída?", + "confirm_passphrase": "Confirme a senha", "confirm_sending": "Confirmar o envio", "confirm_silent_payments_switch_node": "Seu nó atual não suporta pagamentos silenciosos \n A Cake Wallet mudará para um nó compatível, apenas para escanear", "confirmations": "Confirmações", @@ -453,6 +454,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Emparelhamento de evento inválido", "passphrase": "Senha (opcional)", + "passphrases_doesnt_match": "Passagases não correspondem, por favor tente novamente", "password": "Senha", "paste": "Colar", "pause_wallet_creation": "A capacidade de criar a Haven Wallet está atualmente pausada.", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 6ffa87b0f..dd3ce9654 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Это действие удалит кошелек. Вы хотите продолжить?", "confirm_fee_deduction": "Подтвердите вычет платы", "confirm_fee_deduction_content": "Согласны ли вы вычесть плату из вывода?", + "confirm_passphrase": "Подтвердите Passfrase", "confirm_sending": "Подтвердить отправку", "confirm_silent_payments_switch_node": "Ваш текущий узел не поддерживает Silent Payments \\ ncake Wallet переключится на совместимый узел, только для сканирования", "confirmations": "Подтверждения", @@ -452,6 +453,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Недействительное событие сопряжения", "passphrase": "Passfrase (необязательно)", + "passphrases_doesnt_match": "Пасфразы не совпадают, попробуйте еще раз", "password": "Пароль", "paste": "Вставить", "pause_wallet_creation": "Возможность создания Haven Wallet в настоящее время приостановлена.", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 4a41989c8..e13e6053e 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "การดำเนินการนี้จะลบกระเป๋านี้ คุณต้องการดำเนินการต่อหรือไม่?", "confirm_fee_deduction": "ยืนยันการหักค่าธรรมเนียม", "confirm_fee_deduction_content": "คุณตกลงที่จะหักค่าธรรมเนียมจากผลลัพธ์หรือไม่?", + "confirm_passphrase": "ยืนยันวลีรหัสผ่าน", "confirm_sending": "ยืนยันการส่ง", "confirm_silent_payments_switch_node": "โหนดปัจจุบันของคุณไม่รองรับการชำระเงินแบบเงียบ \\ ncake กระเป๋าเงินจะเปลี่ยนเป็นโหนดที่เข้ากันได้เพียงเพื่อการสแกน", "confirmations": "การยืนยัน", @@ -451,6 +452,7 @@ "overwrite_amount": "เขียนทับจำนวน", "pairingInvalidEvent": "การจับคู่เหตุการณ์ที่ไม่ถูกต้อง", "passphrase": "ข้อความรหัสผ่าน (ไม่บังคับ)", + "passphrases_doesnt_match": "Passphrases ไม่ตรงกันโปรดลองอีกครั้ง", "password": "รหัสผ่าน", "paste": "วาง", "pause_wallet_creation": "ขณะนี้ความสามารถในการสร้าง Haven Wallet ถูกหยุดชั่วคราว", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index f2caa5fcc..3ec6160ed 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Tatanggalin ng pagkilos na ito ang wallet na ito. Gusto mo bang magpatuloy?", "confirm_fee_deduction": "Kumpirmahin ang pagbabawas ng fee", "confirm_fee_deduction_content": "Sumasang-ayon ka bang bawasan ang fee mula sa output?", + "confirm_passphrase": "Kumpirma ang passphrase", "confirm_sending": "Kumpirmahin ang pagpapadala", "confirm_silent_payments_switch_node": "Ang iyong kasalukuyang node ay hindi sumusuporta sa tahimik na pagbabayad \\ nCake Wallet ay lilipat sa isang katugmang node, para lamang sa pag-scan", "confirmations": "Mga kumpirmasyon", @@ -451,6 +452,7 @@ "overwrite_amount": "I-overwrite ang halaga", "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (opsyonal)", + "passphrases_doesnt_match": "Ang mga passphrases ay hindi tumutugma, mangyaring subukang muli", "password": "Password", "paste": "I-paste", "pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 83545e261..ab2159c95 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Bu eylem, bu cüzdanı silecek. Devam etmek istiyor musun?", "confirm_fee_deduction": "Ücret kesintisini onaylayın", "confirm_fee_deduction_content": "Ücreti çıktıdan düşürmeyi kabul ediyor musunuz?", + "confirm_passphrase": "Parola onaylayın", "confirm_sending": "Göndermeyi onayla", "confirm_silent_payments_switch_node": "Mevcut düğümünüz sessiz ödemeleri desteklemiyor \\ nCake cüzdanı, sadece tarama için uyumlu bir düğüme geçecektir", "confirmations": "Onay", @@ -451,6 +452,7 @@ "overwrite_amount": "Miktarın üzerine yaz", "pairingInvalidEvent": "Geçersiz Etkinliği Eşleştirme", "passphrase": "Passfrase (isteğe bağlı)", + "passphrases_doesnt_match": "Passfrases eşleşmiyor, lütfen tekrar deneyin", "password": "Parola", "paste": "Yapıştır", "pause_wallet_creation": "Haven Cüzdanı oluşturma yeteneği şu anda duraklatıldı.", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index dc6baad6d..1d6e05dee 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Ця дія видалить гаманець. Ви хочете продовжити?", "confirm_fee_deduction": "Підтвердьте відрахування комісії", "confirm_fee_deduction_content": "Чи погоджуєтесь ви вирахувати комісію з сумми одержувача?", + "confirm_passphrase": "Підтвердьте пасфрази", "confirm_sending": "Підтвердити відправлення", "confirm_silent_payments_switch_node": "Ваш поточний вузол не підтримує мовчазні платежі \\ ncake Wallet перейде на сумісний вузол, лише для сканування", "confirmations": "Підтвердження", @@ -451,6 +452,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Недійсна подія сполучення", "passphrase": "Пасофрази (необов’язково)", + "passphrases_doesnt_match": "Пасофрази не відповідають, спробуйте ще раз", "password": "Пароль", "paste": "Вставити", "pause_wallet_creation": "Можливість створення гаманця Haven зараз призупинено.", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 2140accbe..bf128ca2a 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "اس کارروائی سے یہ پرس حذف ہو جائے گا۔ کیا آپ جاری رکھنا چاہتے ہیں؟", "confirm_fee_deduction": "فیس میں کٹوتی کی تصدیق کریں", "confirm_fee_deduction_content": "کیا آپ آؤٹ پٹ سے فیس کم کرنے پر راضی ہیں؟", + "confirm_passphrase": "پاسفریز کی تصدیق کریں", "confirm_sending": "بھیجنے کی تصدیق کریں۔", "confirm_silent_payments_switch_node": "آپ کا موجودہ نوڈ خاموش ادائیگیوں کی حمایت نہیں کرتا ہے۔", "confirmations": "تصدیقات", @@ -453,6 +454,7 @@ "overwrite_amount": "رقم کو اوور رائٹ کریں۔", "pairingInvalidEvent": "ﭧﻧﻮﯾﺍ ﻂﻠﻏ ﺎﻧﺎﻨﺑ ﺍﮌﻮﺟ", "passphrase": "پاسفریز (اختیاری)", + "passphrases_doesnt_match": "پاسفریز مماثل نہیں ہیں ، براہ کرم دوبارہ کوشش کریں", "password": "پاس ورڈ", "paste": "چسپاں کریں۔", "pause_wallet_creation": "Haven Wallet ۔ﮯﮨ ﻑﻮﻗﻮﻣ ﻝﺎﺤﻟﺍ ﯽﻓ ﺖﯿﻠﮨﺍ ﯽﮐ ﮯﻧﺎﻨﺑ", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index a21da9e68..fc573451e 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -144,6 +144,7 @@ "confirm_fee_dedction_content": "Bạn có đồng ý trừ phí từ đầu ra không?", "confirm_fee_deduction": "Xác nhận Khấu trừ Phí", "confirm_fee_deduction_content": "Bạn có đồng ý khấu trừ phí từ đầu ra không?", + "confirm_passphrase": "Xác nhận cụm mật khẩu", "confirm_sending": "Xác nhận gửi", "confirm_silent_payments_switch_node": "Nút hiện tại của bạn không hỗ trợ thanh toán im lặng\\nCake Wallet sẽ chuyển sang một nút tương thích chỉ để quét", "confirmations": "Xác nhận", @@ -452,6 +453,7 @@ "overwrite_amount": "Ghi đè số tiền", "pairingInvalidEvent": "Sự kiện ghép nối không hợp lệ", "passphrase": "Cụm từ bảo mật (Tùy chọn)", + "passphrases_doesnt_match": "Vòng thông không khớp, vui lòng thử lại", "password": "Mật khẩu", "paste": "Dán", "pause_wallet_creation": "Khả năng tạo ví Haven hiện đang bị tạm dừng.", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 8a30c5e08..1b8dfc065 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Ìṣe yìí máa yọ àpamọ́wọ́ yìí kúrò. Ṣé ẹ fẹ́ tẹ̀síwájú?", "confirm_fee_deduction": "Jẹrisi iyọkuro owo", "confirm_fee_deduction_content": "Ṣe o gba lati yọkuro idiyele naa kuro ni iṣejade?", + "confirm_passphrase": "Jẹrisi kọwe", "confirm_sending": "Jẹ́rìí sí ránṣẹ́", "confirm_silent_payments_switch_node": "Ilode rẹ ti lọwọlọwọ ko ṣe atilẹyin awọn sisanwo ti o dakẹ \\ owet apamọwọ yoo yipada si oju-ọrọ ibaramu, o kan fun Scning", "confirmations": "Àwọn ẹ̀rí", @@ -452,6 +453,7 @@ "overwrite_amount": "Pààrọ̀ iye owó", "pairingInvalidEvent": "Pipọpọ Iṣẹlẹ Ti ko tọ", "passphrase": "Ọrọ kukuru (iyan)", + "passphrases_doesnt_match": "Awọn ọrọ kukuru ko baamu, jọwọ gbiyanju lẹẹkansi", "password": "Ọ̀rọ̀ aṣínà", "paste": "Fikún ẹ̀dà yín", "pause_wallet_creation": "Agbara lati ṣẹda Haven Wallet ti wa ni idaduro lọwọlọwọ.", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 6a3b9d4c8..9564b5982 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "此操作将刪除此钱包。确定吗?", "confirm_fee_deduction": "确认费用扣除", "confirm_fee_deduction_content": "您是否同意从产出中扣除费用?", + "confirm_passphrase": "确认密码", "confirm_sending": "确认发送", "confirm_silent_payments_switch_node": "您当前的节点不支持无声付款\\ ncake钱包将切换到兼容节点,仅用于扫描", "confirmations": "确认", @@ -451,6 +452,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "配对无效事件", "passphrase": "密码(可选)", + "passphrases_doesnt_match": "密码不匹配,请重试", "password": "密码", "paste": "粘贴", "pause_wallet_creation": "创建 Haven 钱包的功能当前已暂停。", diff --git a/tool/append_translation.dart b/tool/append_translation.dart index 24778746c..d196421e9 100644 --- a/tool/append_translation.dart +++ b/tool/append_translation.dart @@ -5,13 +5,14 @@ import 'utils/translation/translation_utils.dart'; /// flutter packages pub run tool/append_translation.dart "hello_world" "Hello World!" void main(List args) async { - if (args.length != 2) { + if (args.length < 2) { throw Exception( - 'Insufficient arguments!\n\nTry to run `./append_translation.dart greetings "Hello World!"`'); + 'Insufficient arguments!\n\nTry to run `./append_translation.dart "greetings" "Hello World!"`'); } final name = args.first; - final text = args.last; + final text = args[1]; + final force = args.last == "--force"; print('Appending "$name": "$text"'); @@ -20,7 +21,7 @@ void main(List args) async { final fileName = getArbFileName(lang); final translation = await getTranslation(text, lang); - appendStringToArbFile(fileName, name, translation); + appendStringToArbFile(fileName, name, translation, force: force); } print('Alphabetizing all files...'); diff --git a/tool/utils/translation/arb_file_utils.dart b/tool/utils/translation/arb_file_utils.dart index b54dab423..414d318dd 100644 --- a/tool/utils/translation/arb_file_utils.dart +++ b/tool/utils/translation/arb_file_utils.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'dart:io'; -void appendStringToArbFile(String fileName, String name, String text) { +void appendStringToArbFile(String fileName, String name, String text, {bool force = false}) { final file = File(fileName); final arbObj = readArbFile(file); - if (arbObj.containsKey(name)) { + if (arbObj.containsKey(name) && !force) { print("String $name already exists in $fileName!"); return; } From 647551661d3fde011a017974eb56ece869c94e98 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 26 Sep 2024 05:10:46 +0300 Subject: [PATCH 095/142] fix derivation Info (#1689) Co-authored-by: Omar Hatem --- lib/view_model/wallet_creation_vm.dart | 45 ++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 7041fa382..9fce86712 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -5,8 +5,10 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/generate_name.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -82,9 +84,22 @@ abstract class WalletCreationVMBase with Store { walletCreationService.checkIfExists(name); final dirPath = await pathForWalletDir(name: name, type: type); final path = await pathForWallet(name: name, type: type); - final credentials = restoreWallet != null - ? getCredentialsFromRestoredWallet(options, restoreWallet) - : getCredentials(options); + + WalletCredentials credentials; + if (restoreWallet != null) { + if (restoreWallet.restoreMode == WalletRestoreMode.seed && + options == null && + (type == WalletType.nano || + type == WalletType.bitcoin || + type == WalletType.litecoin)) { + final derivationInfo = await getDerivationInfo(restoreWallet); + options ??= {}; + options["derivationInfo"] = derivationInfo.first; + } + credentials = getCredentialsFromRestoredWallet(options, restoreWallet); + } else { + credentials = getCredentials(options); + } final walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -185,6 +200,30 @@ abstract class WalletCreationVMBase with Store { } } + Future> getDerivationInfo(RestoredWallet restoreWallet) async { + var list = []; + final walletType = restoreWallet.type; + var appStore = getIt.get(); + var node = appStore.settingsStore.getCurrentNode(walletType); + + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + return bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; + } + WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); Future process(WalletCredentials credentials) => throw UnimplementedError(); From 9edd87b0afa330f2d3aecfcf3dad9a8bfcb0048c Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Wed, 25 Sep 2024 19:15:09 -0700 Subject: [PATCH 096/142] keep sign/verify pages loaded (#1703) --- lib/src/screens/dashboard/widgets/sign_form.dart | 6 +++++- lib/src/screens/dashboard/widgets/verify_form.dart | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/src/screens/dashboard/widgets/sign_form.dart b/lib/src/screens/dashboard/widgets/sign_form.dart index c0f8ba328..deead87a4 100644 --- a/lib/src/screens/dashboard/widgets/sign_form.dart +++ b/lib/src/screens/dashboard/widgets/sign_form.dart @@ -20,7 +20,7 @@ class SignForm extends StatefulWidget { SignFormState createState() => SignFormState(); } -class SignFormState extends State { +class SignFormState extends State with AutomaticKeepAliveClientMixin { SignFormState() : formKey = GlobalKey(), messageController = TextEditingController(), @@ -42,8 +42,12 @@ class SignFormState extends State { super.dispose(); } + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { + super.build(context); return Container( padding: EdgeInsets.only(left: 24, right: 24), child: Column( diff --git a/lib/src/screens/dashboard/widgets/verify_form.dart b/lib/src/screens/dashboard/widgets/verify_form.dart index bf6809586..282ffa9da 100644 --- a/lib/src/screens/dashboard/widgets/verify_form.dart +++ b/lib/src/screens/dashboard/widgets/verify_form.dart @@ -15,7 +15,7 @@ class VerifyForm extends StatefulWidget { VerifyFormState createState() => VerifyFormState(); } -class VerifyFormState extends State { +class VerifyFormState extends State with AutomaticKeepAliveClientMixin { VerifyFormState() : formKey = GlobalKey(), messageController = TextEditingController(), @@ -36,9 +36,13 @@ class VerifyFormState extends State { void dispose() { super.dispose(); } + + @override + bool get wantKeepAlive => true; @override Widget build(BuildContext context) { + super.build(context); return Container( padding: EdgeInsets.only(left: 24, right: 24), child: Form( From 331d6ee64836f52486a7fb43aee2adddd7efb873 Mon Sep 17 00:00:00 2001 From: MyEcoria <103332030+MyEcoria@users.noreply.github.com> Date: Thu, 26 Sep 2024 04:16:31 +0200 Subject: [PATCH 097/142] Update transaction_details_view_model.dart (#1690) Replacement proposal for the closed nano and banano explorers --- lib/view_model/transaction_details_view_model.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index d3aa2a841..a96d70a90 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -160,9 +160,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.ethereum: return 'https://etherscan.io/tx/${txId}'; case WalletType.nano: - return 'https://nanolooker.com/block/${txId}'; + return 'https://nanexplorer.com/nano/block/${txId}'; case WalletType.banano: - return 'https://bananolooker.com/block/${txId}'; + return 'https://nanexplorer.com/banano/block/${txId}'; case WalletType.polygon: return 'https://polygonscan.com/tx/${txId}'; case WalletType.solana: @@ -190,9 +190,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.ethereum: return S.current.view_transaction_on + 'etherscan.io'; case WalletType.nano: - return S.current.view_transaction_on + 'nanolooker.com'; + return S.current.view_transaction_on + 'nanexplorer.com'; case WalletType.banano: - return S.current.view_transaction_on + 'bananolooker.com'; + return S.current.view_transaction_on + 'nanexplorer.com'; case WalletType.polygon: return S.current.view_transaction_on + 'polygonscan.com'; case WalletType.solana: From c6b0b7ff4e825a503b9ec0245c84cb7d5e09e09d Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Fri, 27 Sep 2024 03:34:33 +0300 Subject: [PATCH 098/142] fix valid seed check for non bip39 wallets --- lib/src/screens/restore/wallet_restore_page.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index c8e109860..4a4e27b3d 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -284,8 +284,11 @@ class WalletRestorePage extends BasePage { } // bip39: - const validSeedLengths = [12, 18, 24]; - if (!(validSeedLengths.contains(seedWords.length))) { + final validBip39SeedLengths = [12, 18, 24]; + final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven]; + // if it's a bip39 wallet and the length is not valid return false + if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) && + !(validBip39SeedLengths.contains(seedWords.length))) { return false; } From 791b410277ce9a3cde321226ca744380be3977c6 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Fri, 27 Sep 2024 04:05:45 +0300 Subject: [PATCH 099/142] add solana node [skip ci] --- assets/solana_node_list.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/solana_node_list.yml b/assets/solana_node_list.yml index e3ff9138e..e5641d3f8 100644 --- a/assets/solana_node_list.yml +++ b/assets/solana_node_list.yml @@ -4,4 +4,7 @@ useSSL: true - uri: api.mainnet-beta.solana.com:443 + useSSL: true +- + uri: solana-rpc.publicnode.com:443 useSSL: true \ No newline at end of file From b2850c203f319a37d7888465a46b83d951383156 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 27 Sep 2024 23:36:56 +0100 Subject: [PATCH 100/142] feat: Add UI signifying the currently selected wallet indicator (#1704) * feat: Add UI signifying the currently selected wallet indicator * fix: Product Fix - Add more padding to child wallets list item tile in grouped wallets, fix sort by dragging for general single wallets, and also groups, add animation to trailing arrow icon when tile is expanded or not * Update wallet group image and change maxLines --------- Co-authored-by: tuxpizza --- assets/images/wallet_group.png | Bin 5053 -> 0 bytes assets/images/wallet_group_bright.png | Bin 0 -> 10698 bytes assets/images/wallet_group_dark.png | Bin 0 -> 7100 bytes assets/images/wallet_group_light.png | Bin 0 -> 7193 bytes .../wallet_group_description_page.dart | 19 +++-- .../new_wallet/wallet_group_display_page.dart | 1 + .../grouped_wallet_expansion_tile.dart | 47 +++++++++++-- .../edit_wallet_button_widget.dart | 4 +- .../screens/wallet_list/wallet_list_page.dart | 66 +++++++++++++++--- .../wallet_list/wallet_list_view_model.dart | 33 ++++++++- 10 files changed, 144 insertions(+), 26 deletions(-) delete mode 100644 assets/images/wallet_group.png create mode 100644 assets/images/wallet_group_bright.png create mode 100644 assets/images/wallet_group_dark.png create mode 100644 assets/images/wallet_group_light.png diff --git a/assets/images/wallet_group.png b/assets/images/wallet_group.png deleted file mode 100644 index f4a577b22a05a172de08fc32b178dc3317047526..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5053 zcmai2g;Nv`(?&oAjt(8WI8-=jXlQtHvXbimbozh9dht)X8t;7jr?4Gl^_
|GG>K1DW2bVv_sRl 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 B*q8tS diff --git a/assets/images/wallet_group_bright.png b/assets/images/wallet_group_bright.png new file mode 100644 index 0000000000000000000000000000000000000000..263361db63617eaf4b9bd5a1379e5605126bd970 GIT binary patch literal 10698 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 Fe*nBVZlnMJ literal 0 HcmV?d00001 diff --git a/assets/images/wallet_group_dark.png b/assets/images/wallet_group_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7cd08d2cd9c0c6f6c4522dec292c005aecf47771 GIT binary patch literal 7100 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 literal 0 HcmV?d00001 diff --git a/assets/images/wallet_group_light.png b/assets/images/wallet_group_light.png new file mode 100644 index 0000000000000000000000000000000000000000..7827971e7d250b226189ce7e30021157c4b0fa92 GIT binary patch literal 7193 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 ZNT;ZX*43|#EmSnn2i*5R+3Wx7e*t#8Re1ma literal 0 HcmV?d00001 diff --git a/lib/src/screens/new_wallet/wallet_group_description_page.dart b/lib/src/screens/new_wallet/wallet_group_description_page.dart index eb3501bed..f30ff1d6c 100644 --- a/lib/src/screens/new_wallet/wallet_group_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -3,10 +3,11 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; - +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:flutter_svg/svg.dart'; class WalletGroupDescriptionPage extends BasePage { WalletGroupDescriptionPage({required this.selectedWalletType}); @@ -16,17 +17,25 @@ class WalletGroupDescriptionPage extends BasePage { @override String get title => S.current.wallet_group; + @override Widget body(BuildContext context) { + + final lightImage = 'assets/images/wallet_group_light.png'; + final darkImage = 'assets/images/wallet_group_dark.png'; + final brightImage = 'assets/images/wallet_group_bright.png'; + + final image = currentTheme.type == ThemeType.light ? lightImage : darkImage; + return Container( alignment: Alignment.center, padding: EdgeInsets.all(24), child: Column( children: [ - Image.asset( - 'assets/images/wallet_group.png', - scale: 0.8, - ), + Image.asset( + image, + height: 200 + ), SizedBox(height: 32), Expanded( child: Text.rich( diff --git a/lib/src/screens/new_wallet/wallet_group_display_page.dart b/lib/src/screens/new_wallet/wallet_group_display_page.dart index 67dde506e..d3ce27446 100644 --- a/lib/src/screens/new_wallet/wallet_group_display_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -58,6 +58,7 @@ class WalletGroupsDisplayBody extends StatelessWidget { final groupName = group.groupName ?? '${S.of(context).wallet_group} ${index + 1}'; return GroupedWalletExpansionTile( + shouldShowCurrentWalletPointer: false, leadingWidget: Icon(Icons.account_balance_wallet_outlined, size: 28), borderRadius: BorderRadius.all(Radius.circular(16)), diff --git a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart index 52e2e74b0..57f6a9047 100644 --- a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart +++ b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart @@ -12,6 +12,7 @@ class GroupedWalletExpansionTile extends StatelessWidget { this.childWallets = const [], this.onTitleTapped, this.onChildItemTapped = _defaultVoidCallback, + this.onExpansionChanged, this.leadingWidget, this.trailingWidget, this.childTrailingWidget, @@ -22,13 +23,18 @@ class GroupedWalletExpansionTile extends StatelessWidget { this.borderRadius, this.margin, this.tileKey, + this.isCurrentlySelectedWallet = false, + this.shouldShowCurrentWalletPointer = false, }) : super(key: tileKey); final Key? tileKey; final bool isSelected; + final bool isCurrentlySelectedWallet; + final bool shouldShowCurrentWalletPointer; final VoidCallback? onTitleTapped; final void Function(WalletListItem item) onChildItemTapped; + final void Function(bool)? onExpansionChanged; final String title; final Widget? leadingWidget; @@ -70,8 +76,10 @@ class GroupedWalletExpansionTile extends StatelessWidget { splashFactory: NoSplash.splashFactory, ), child: ExpansionTile( + onExpansionChanged: onExpansionChanged, key: tileKey, - tilePadding: EdgeInsets.symmetric(vertical: 1, horizontal: 16), + tilePadding: + EdgeInsets.symmetric(vertical: 1, horizontal: !isCurrentlySelectedWallet ? 16 : 0), iconColor: effectiveArrowColor, collapsedIconColor: effectiveArrowColor, leading: leadingWidget, @@ -90,19 +98,46 @@ class GroupedWalletExpansionTile extends StatelessWidget { ), children: childWallets.map( (item) { + final currentColor = item.isCurrent + ? Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor + : Theme.of(context).colorScheme.background; final walletTypeToCrypto = walletTypeToCryptoCurrency(item.type); return ListTile( + contentPadding: EdgeInsets.zero, key: ValueKey(item.name), trailing: childTrailingWidget?.call(item), onTap: () => onChildItemTapped(item), - leading: Image.asset( - walletTypeToCrypto.iconPath!, - width: 32, - height: 32, + leading: SizedBox( + width: 60, + child: Row( + children: [ + item.isCurrent && shouldShowCurrentWalletPointer + ? Container( + height: 35, + width: 6, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + color: currentColor, + ), + ) + : SizedBox(width: 6), + SizedBox(width: 16), + Image.asset( + walletTypeToCrypto.iconPath!, + width: 32, + height: 32, + ), + ], + ), ), title: Text( item.name, - maxLines: 1, + maxLines: 2, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, diff --git a/lib/src/screens/wallet_list/edit_wallet_button_widget.dart b/lib/src/screens/wallet_list/edit_wallet_button_widget.dart index 8f858348a..fd6f1a0ee 100644 --- a/lib/src/screens/wallet_list/edit_wallet_button_widget.dart +++ b/lib/src/screens/wallet_list/edit_wallet_button_widget.dart @@ -7,11 +7,13 @@ class EditWalletButtonWidget extends StatelessWidget { required this.width, required this.onTap, this.isGroup = false, + this.isExpanded = false, super.key, }); final bool isGroup; final double width; + final bool isExpanded; final VoidCallback onTap; @override @@ -42,7 +44,7 @@ class EditWalletButtonWidget extends StatelessWidget { if (isGroup) ...{ SizedBox(width: 6), Icon( - Icons.keyboard_arrow_down, + isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 24, color: Theme.of(context).extension()!.titlesColor, ), diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 9c210d8eb..8bb9593c9 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -156,7 +157,18 @@ class WalletListBodyState extends State { final group = widget.walletListViewModel.multiWalletGroups[index]; final groupName = group.groupName ?? '${S.current.wallet_group} ${index + 1}'; + + widget.walletListViewModel.updateTileState( + index, + widget.walletListViewModel.expansionTileStateTrack[index] ?? false, + ); + return GroupedWalletExpansionTile( + onExpansionChanged: (value) { + widget.walletListViewModel.updateTileState(index, value); + setState(() {}); + }, + shouldShowCurrentWalletPointer: true, borderRadius: BorderRadius.all(Radius.circular(16)), margin: EdgeInsets.only(left: 20, right: 20, bottom: 12), title: groupName, @@ -168,6 +180,8 @@ class WalletListBodyState extends State { trailingWidget: EditWalletButtonWidget( width: 74, isGroup: true, + isExpanded: + widget.walletListViewModel.expansionTileStateTrack[index]!, onTap: () { final wallet = widget.walletListViewModel .convertWalletInfoToWalletListItem(group.wallets.first); @@ -193,13 +207,16 @@ class WalletListBodyState extends State { childTrailingWidget: (item) { return item.isCurrent ? SizedBox.shrink() - : EditWalletButtonWidget( - width: 44, - onTap: () => Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: item, + : Padding( + padding: const EdgeInsets.only(right: 16), + child: EditWalletButtonWidget( + width: 44, + onTap: () => Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: item, + ), ), ), ); @@ -232,13 +249,40 @@ class WalletListBodyState extends State { updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, itemBuilder: (context, index) { final wallet = widget.walletListViewModel.singleWalletsList[index]; + final currentColor = wallet.isCurrent + ? Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor + : Theme.of(context).colorScheme.background; return GroupedWalletExpansionTile( tileKey: ValueKey('single_wallets_expansion_tile_widget_$index'), - leadingWidget: Image.asset( - walletTypeToCryptoCurrency(wallet.type).iconPath!, - width: 32, - height: 32, + isCurrentlySelectedWallet: wallet.isCurrent, + leadingWidget: SizedBox( + width: 60, + child: Row( + children: [ + wallet.isCurrent + ? Container( + height: 35, + width: 6, + margin: EdgeInsets.only(right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + color: currentColor, + ), + ) + : SizedBox(width: 6), + Image.asset( + walletTypeToCryptoCurrency(wallet.type).iconPath!, + width: 32, + height: 32, + ), + ], + ), ), title: wallet.name, isSelected: false, diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 539f5c640..4a4fdcb19 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -22,7 +22,8 @@ abstract class WalletListViewModelBase with Store { this._walletManager, ) : wallets = ObservableList(), multiWalletGroups = ObservableList(), - singleWalletsList = ObservableList() { + singleWalletsList = ObservableList(), + expansionTileStateTrack = ObservableMap() { setOrderType(_appStore.settingsStore.walletListOrder); reaction((_) => _appStore.wallet, (_) => updateList()); updateList(); @@ -40,6 +41,18 @@ abstract class WalletListViewModelBase with Store { @observable ObservableList singleWalletsList; + @observable + ObservableMap expansionTileStateTrack; + + @action + void updateTileState(int index, bool isExpanded) { + if (expansionTileStateTrack.containsKey(index)) { + expansionTileStateTrack.update(index, (value) => isExpanded); + } else { + expansionTileStateTrack.addEntries({index: isExpanded}.entries); + } + } + @computed bool get shouldRequireTOTP2FAForAccessingWallet => _appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; @@ -100,8 +113,8 @@ abstract class WalletListViewModelBase with Store { // delete all wallets from walletInfoSource: await _walletInfoSource.clear(); - // add wallets from wallets list in order of wallets list, by name: - for (WalletListItem wallet in wallets) { + // Reorder single wallets using the singleWalletsList + for (WalletListItem wallet in singleWalletsList) { for (int i = 0; i < walletInfoSourceCopy.length; i++) { if (walletInfoSourceCopy[i].name == wallet.name) { await _walletInfoSource.add(walletInfoSourceCopy[i]); @@ -111,6 +124,20 @@ abstract class WalletListViewModelBase with Store { } } + // Reorder wallets within multi-wallet groups + for (WalletGroup group in multiWalletGroups) { + for (WalletInfo walletInfo in group.wallets) { + for (int i = 0; i < walletInfoSourceCopy.length; i++) { + if (walletInfoSourceCopy[i].name == walletInfo.name) { + await _walletInfoSource.add(walletInfoSourceCopy[i]); + walletInfoSourceCopy.removeAt(i); + break; + } + } + } + } + + // Rebuild the list of wallets and groups updateList(); } From b6e8970dcac888f13c23a25314ae068dfea55adb Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sat, 28 Sep 2024 01:58:10 +0300 Subject: [PATCH 101/142] change tags to POL and fix lets exchange (#1708) --- cw_core/lib/crypto_currency.dart | 8 ++++---- lib/exchange/provider/letsexchange_exchange_provider.dart | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index d3ef6526a..4f9b5d835 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -174,11 +174,11 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png', decimals: 8); static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png', decimals: 8); - static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POLY', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POL', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/dcr_icon.png', decimals: 8); static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png', decimals: 8); static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png', decimals: 18); - static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18); + static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POL', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18); static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/matic_icon.png', decimals: 18); static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png', decimals: 18); static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png', decimals: 24); @@ -215,8 +215,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png', decimals: 18); static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png', decimals: 18); static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29); - static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6); - static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POL', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6); + static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POL', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart index a11e69796..1d4da55cb 100644 --- a/lib/exchange/provider/letsexchange_exchange_provider.dart +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -278,8 +278,6 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { return 'ERC20'; case 'BSC': return 'BEP20'; - case 'POLY': - return 'MATIC'; default: return currency.tag!; } From 4c55d9664763adef4443202bf2f0d4b0283e868c Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 28 Sep 2024 02:02:37 +0300 Subject: [PATCH 102/142] reset address balances before updating unspents (#1707) --- cw_bitcoin/lib/electrum_wallet.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 71e537186..f291247b8 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1272,6 +1272,11 @@ abstract class ElectrumWalletBase }); } + // Set the balance of all non-silent payment addresses to 0 before updating + walletAddresses.allAddresses.forEach((addr) { + if(addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + }); + await Future.wait(walletAddresses.allAddresses.map((address) async { updatedUnspentCoins.addAll(await fetchUnspent(address)); })); From a149c71fb22b6e3e47de6ebd56b271696b45f538 Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 28 Sep 2024 01:07:40 +0200 Subject: [PATCH 103/142] Update monero_c (#1709) This fixes the issue where coin control doesn't work as expected, and ignores the preferred inputs --- cw_monero/pubspec.lock | 4 ++-- cw_monero/pubspec.yaml | 2 +- cw_wownero/pubspec.lock | 4 ++-- cw_wownero/pubspec.yaml | 2 +- scripts/prepare_moneroc.sh | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 360d33ae3..ee1d48df1 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" - resolved-ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" + ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" 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 75d8b1ccc..cb1f5519f 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 # monero_c hash + ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 0e97b3214..c90340800 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" - resolved-ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" + ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" 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 39a2cc9c0..6943e60c3 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: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 # monero_c hash + ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 1315a5825..24f4d201c 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,7 +8,7 @@ if [[ ! -d "monero_c" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch rewrite-wip cd monero_c - git checkout 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 + git checkout 6eb571ea498ed7b854934785f00fabfd0dadf75b git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero From fc5878d9912a0e8f85880b83fd618ab6c9dfd711 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Sat, 28 Sep 2024 01:04:10 +0100 Subject: [PATCH 104/142] Currently selected wallet pointer UI (#1710) * feat: Add UI signifying the currently selected wallet indicator * fix: Product Fix - Add more padding to child wallets list item tile in grouped wallets, fix sort by dragging for general single wallets, and also groups, add animation to trailing arrow icon when tile is expanded or not * Update wallet group image and change maxLines * fix: Make group tile expand if it has the currently selected wallet and fix issue with wallet group image --------- Co-authored-by: tuxpizza --- .../wallet_group_description_page.dart | 23 +++++++++++++++---- .../grouped_wallet_expansion_tile.dart | 3 +++ .../screens/wallet_list/wallet_list_page.dart | 2 +- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/src/screens/new_wallet/wallet_group_description_page.dart b/lib/src/screens/new_wallet/wallet_group_description_page.dart index f30ff1d6c..5becea3a5 100644 --- a/lib/src/screens/new_wallet/wallet_group_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -32,10 +32,10 @@ class WalletGroupDescriptionPage extends BasePage { padding: EdgeInsets.all(24), child: Column( children: [ - Image.asset( - image, - height: 200 - ), + Image.asset( + _getThemedWalletGroupImage(currentTheme.type), + height: 200, + ), SizedBox(height: 32), Expanded( child: Text.rich( @@ -96,4 +96,19 @@ class WalletGroupDescriptionPage extends BasePage { ), ); } + + String _getThemedWalletGroupImage(ThemeType theme) { + final lightImage = 'assets/images/wallet_group_light.png'; + final darkImage = 'assets/images/wallet_group_dark.png'; + final brightImage = 'assets/images/wallet_group_bright.png'; + + switch (theme) { + case ThemeType.bright: + return brightImage; + case ThemeType.light: + return lightImage; + default: + return darkImage; + } + } } diff --git a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart index 57f6a9047..74057cc87 100644 --- a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart +++ b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart @@ -77,6 +77,9 @@ class GroupedWalletExpansionTile extends StatelessWidget { ), child: ExpansionTile( onExpansionChanged: onExpansionChanged, + initiallyExpanded: shouldShowCurrentWalletPointer + ? childWallets.any((element) => element.isCurrent) + : false, key: tileKey, tilePadding: EdgeInsets.symmetric(vertical: 1, horizontal: !isCurrentlySelectedWallet ? 16 : 0), diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 8bb9593c9..d17534f6b 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -259,7 +259,7 @@ class WalletListBodyState extends State { tileKey: ValueKey('single_wallets_expansion_tile_widget_$index'), isCurrentlySelectedWallet: wallet.isCurrent, leadingWidget: SizedBox( - width: 60, + width: wallet.isCurrent ? 56 : 40, child: Row( children: [ wallet.isCurrent From 62e0c2a5928994b74219730291bf4113f23d3625 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Fri, 27 Sep 2024 19:22:25 -0700 Subject: [PATCH 105/142] litecoin mweb support (#1455) * Fix stub creation * Generate MWEB addresses * Fix mweb address derivation * Use camel-case * Show utxos in tx list * A few fixes * Add spent processing * Update balance * Balance fixes * Update address records * Get rid of debounce hack * Get sending up to the confirmation box * Fee estimation * Stop the daemon if plugin is unloaded * Normal fee for non-mweb txns * Fix fee estimation for send all * Don't hash mweb addresses * More fee fixes * Broadcast mweb * Remove test files * One more * Confirm sent txns * Couple of fixes * Resign inputs after mweb create * Some more fixes * Update balance after sending * Correctly update address records * Update confs * [skip ci] updates * [skip ci] add dep overrides * working * small fix * merge fixes [skip ci] * merge fixes [skip ci] * [skip ci] minor fixes * silent payment fixes [skip ci] * updates [skip ci] * save [skip ci] * use mwebutxos box * [skip ci] lots of fixes, still testing * add rescan from height feature and test workflow build * install go * use sudo * correct package name * move building mweb higher for faster testing * install fixes * install later version of go * go fixes * testing * testing * testing * testing * testing * should workgit add .github/workflows/pr_test_build.yml * ??? * ??? pt.2 * should work, for real this time * fix tx history not persisting + update build_mwebd script * updates * fix some rescan and address gen issues * save [skip ci] * fix unconfirmed balance not updating when receiving * unspent coins / coin control fixes * coin control fixes * address balance and txCount fixes, try/catch electrum call * fix txCount for addresses * save [skip ci] * potential fixes * minor fix * minor fix - 2 * sync status fixes, potential fix for background state issue * workflow and script updates * updates * expirimental optimization * [skip ci] minor enhancements * workflow and script fixes * workflow minor cleanup [skip ci] * minor code cleanup & friendlier error message on failed tx's * balance when sending fix * experimental * more experiments * save * updates * coin control edge cases * remove neutrino.db if no litecoin wallets left after deleting * update translations * updates * minor fix * [skip ci] update translations + minor fixes * state fixes * configure fix * ui updates * translation fixes * [skip ci] addressbook updates * fix popup * fix popup2 * fix litecoin address book * fix ios mwebd build script * fix for building monero.com * minor fix * uncomment fix for state issues * potential mweb sync fix (ios) * remove print [skip ci] * electrum stream potential fix * fix ios build issues [skip ci] * connection reliability updates, update kotlin code to match swift code, minor electrum error handling * dep fixes * minor fix * more merge fixes * bitcoin_flutter removal fixes * [skip ci] fix always scan setting, swift updates * updates * fixes * small fix * small fix * fix * dart:convert != package:convert * change address fixes * update bitcoin_base to fix mweb address program checking * fix ios xcode project [skip ci] * updates * more fixes * more fixes * ensure we don't initialize mweb until we really have to * fix regression * improve mweb reliability * [skip ci] wip adress generation * wip * wip * [skip ci] wip * updates [skip ci] * ios fixes * fix workflows + ios fix * test old mweb version * update go version and mwebd hash * review updates pt.1 * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * remove non-litecoin address types regex [skip ci] * more minor fixes * remove duplicate [skip ci] * Update lib/store/settings_store.dart Co-authored-by: Omar Hatem * script updates, swap params on createLitecoinWalletService * topup fix * [skip ci] wip * [skip ci] testing * [skip ci] file didn't get saved * more address generation reliability fixes * [skip ci] minor * minor code cleanup * hopefully prevents send issue * [skip ci] wip address changes * [skip ci] save * save mweb addresses, auto-restart sync process if it gets stuck [skip ci] * address generation issues mostly resolved * more performance fixes * [skip ci] * this should maybe be refactored, pt.1 * separate mweb balances, pt.2 * [skip ci] save * add translations [skip ci] * fix sending with mweb amounts * works for simple mweb-mweb case, further testing needed * found an edge case * [skip ci] make failed broadcast error message less serious * minor * capture all grpc errors and much better error handling overall * [skip ci] minor * prevent transactions with < 6 confirmations from being used + hide mweb balances if mweb is off * fix * merge fixes pt.1 [skip ci] * fix mweb tags * fix * [skip ci] fix tag spacing * fix transaction history not showing up * fix mweb crash on non-fully deleted mweb cache, sync status ETA, other connection fixes * [skip ci] minor code cleanup * [skip ci] minor code cleanup * additional cleanup * silent payments eta fixes and updates * revert sync eta changes into separate pr * [skip ci] minor * [skip ci] minor * revert sync status title * review fixes, additional cleanup * [skip ci] minor * [skip ci] minor * [skip ci] minor * trigger build * review fixes, pt.2 * check if still processing utxos before updating sync status [skip ci] * [skip ci] minor * balance fix * minor * minor * [skip ci] minor * [skip ci] fix test net btc * don't use mwebd for non-mweb tx's * [skip ci] minor cleanup * don't show all 1000+ mweb addresses on receive page * minor cleanup + additional logging --------- Co-authored-by: Hector Chu Co-authored-by: Omar Hatem Co-authored-by: Czarek Nakamoto --- .github/workflows/pr_test_build_android.yml | 19 + .github/workflows/pr_test_build_linux.yml | 19 + assets/images/mweb_logo.png | Bin 0 -> 19409 bytes cw_bitcoin/lib/bitcoin_mnemonic.dart | 3 +- .../lib/bitcoin_receive_page_option.dart | 10 + .../lib/bitcoin_transaction_priority.dart | 2 +- cw_bitcoin/lib/electrum.dart | 6 + cw_bitcoin/lib/electrum_balance.dart | 33 +- cw_bitcoin/lib/electrum_wallet.dart | 262 +++--- cw_bitcoin/lib/electrum_wallet_addresses.dart | 221 +++-- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 13 +- cw_bitcoin/lib/litecoin_wallet.dart | 848 +++++++++++++++++- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 149 ++- cw_bitcoin/lib/litecoin_wallet_service.dart | 28 +- .../lib/pending_bitcoin_transaction.dart | 39 +- cw_bitcoin/pubspec.lock | 31 + cw_bitcoin/pubspec.yaml | 12 +- cw_bitcoin_cash/pubspec.yaml | 2 +- cw_core/lib/balance.dart | 10 +- cw_core/lib/get_height_by_date.dart | 5 + cw_core/lib/hive_type_ids.dart | 1 + cw_core/lib/mweb_utxo.dart | 33 + cw_core/lib/sync_status.dart | 2 +- cw_core/lib/wallet_base.dart | 2 + cw_haven/android/build.gradle | 1 - cw_mweb/.gitignore | 30 + cw_mweb/.metadata | 36 + cw_mweb/CHANGELOG.md | 3 + cw_mweb/LICENSE | 1 + cw_mweb/README.md | 15 + cw_mweb/analysis_options.yaml | 4 + cw_mweb/android/.gitignore | 10 + cw_mweb/android/build.gradle | 76 ++ cw_mweb/android/settings.gradle | 1 + cw_mweb/android/src/main/AndroidManifest.xml | 3 + .../com/cakewallet/mweb/CwMwebPlugin.kt | 58 ++ cw_mweb/ios/.gitignore | 38 + cw_mweb/ios/Assets/.gitkeep | 0 cw_mweb/ios/Classes/CwMwebPlugin.swift | 86 ++ cw_mweb/ios/cw_mweb.podspec | 26 + cw_mweb/lib/cw_mweb.dart | 133 +++ cw_mweb/lib/cw_mweb_method_channel.dart | 32 + cw_mweb/lib/cw_mweb_platform_interface.dart | 39 + cw_mweb/lib/mwebd.pb.dart | 801 +++++++++++++++++ cw_mweb/lib/mwebd.pbgrpc.dart | 159 ++++ cw_mweb/macos/Classes/CwMwebPlugin.swift | 19 + cw_mweb/macos/cw_mweb.podspec | 23 + cw_mweb/pubspec.yaml | 76 ++ ios/.gitignore | 2 + ios/Podfile.lock | 5 + ios/Runner.xcodeproj/project.pbxproj | 6 + lib/bitcoin/cw_bitcoin.dart | 53 +- lib/core/address_validator.dart | 21 +- lib/core/wallet_loading_service.dart | 3 +- lib/di.dart | 33 +- lib/entities/preferences_key.dart | 4 + lib/main.dart | 6 +- lib/router.dart | 5 + lib/routes.dart | 1 + .../screens/dashboard/pages/address_page.dart | 3 +- .../screens/dashboard/pages/balance_page.dart | 236 ++++- .../dashboard/pages/transactions_page.dart | 16 +- .../dashboard/widgets/menu_widget.dart | 5 + .../dashboard/widgets/transaction_raw.dart | 7 +- .../screens/receive/widgets/address_list.dart | 3 + lib/src/screens/rescan/rescan_page.dart | 2 + lib/src/screens/root/root.dart | 8 + .../desktop_settings_page.dart | 5 + lib/src/screens/settings/mweb_settings.dart | 51 ++ .../widgets/unspent_coins_list_item.dart | 87 +- lib/src/widgets/blockchain_height_widget.dart | 6 +- lib/src/widgets/dashboard_card_widget.dart | 2 +- lib/src/widgets/setting_actions.dart | 11 + lib/store/settings_store.dart | 47 + lib/utils/exception_handler.dart | 2 +- .../dashboard/balance_view_model.dart | 72 +- .../dashboard/dashboard_view_model.dart | 42 +- .../dashboard/receive_option_view_model.dart | 30 +- .../dashboard/transaction_list_item.dart | 10 +- lib/view_model/rescan_view_model.dart | 2 + lib/view_model/send/send_view_model.dart | 5 +- .../settings/mweb_settings_view_model.dart | 32 + .../unspent_coins_list_view_model.dart | 38 +- .../wallet_address_list_view_model.dart | 31 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + model_generator.sh | 1 + pubspec_base.yaml | 8 +- res/values/strings_ar.arb | 11 + res/values/strings_bg.arb | 11 + res/values/strings_cs.arb | 11 + res/values/strings_de.arb | 11 + res/values/strings_en.arb | 11 + res/values/strings_es.arb | 11 + res/values/strings_fr.arb | 11 + res/values/strings_ha.arb | 11 + res/values/strings_hi.arb | 11 + res/values/strings_hr.arb | 11 + res/values/strings_hy.arb | 3 + res/values/strings_id.arb | 11 + res/values/strings_it.arb | 11 + res/values/strings_ja.arb | 11 + res/values/strings_ko.arb | 11 + res/values/strings_my.arb | 11 + res/values/strings_nl.arb | 11 + res/values/strings_pl.arb | 11 + res/values/strings_pt.arb | 11 + res/values/strings_ru.arb | 11 + res/values/strings_th.arb | 11 + res/values/strings_tl.arb | 11 + res/values/strings_tr.arb | 11 + res/values/strings_uk.arb | 11 + res/values/strings_ur.arb | 11 + res/values/strings_vi.arb | 3 + res/values/strings_yo.arb | 11 + res/values/strings_zh.arb | 11 + scripts/android/build_all.sh | 3 +- scripts/android/build_mwebd.sh | 22 + scripts/ios/build_all.sh | 2 +- scripts/ios/build_mwebd.sh | 20 + tool/configure.dart | 11 +- 120 files changed, 4308 insertions(+), 365 deletions(-) create mode 100644 assets/images/mweb_logo.png create mode 100644 cw_core/lib/mweb_utxo.dart create mode 100644 cw_mweb/.gitignore create mode 100644 cw_mweb/.metadata create mode 100644 cw_mweb/CHANGELOG.md create mode 100644 cw_mweb/LICENSE create mode 100644 cw_mweb/README.md create mode 100644 cw_mweb/analysis_options.yaml create mode 100644 cw_mweb/android/.gitignore create mode 100644 cw_mweb/android/build.gradle create mode 100644 cw_mweb/android/settings.gradle create mode 100644 cw_mweb/android/src/main/AndroidManifest.xml create mode 100644 cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt create mode 100644 cw_mweb/ios/.gitignore create mode 100644 cw_mweb/ios/Assets/.gitkeep create mode 100644 cw_mweb/ios/Classes/CwMwebPlugin.swift create mode 100644 cw_mweb/ios/cw_mweb.podspec create mode 100644 cw_mweb/lib/cw_mweb.dart create mode 100644 cw_mweb/lib/cw_mweb_method_channel.dart create mode 100644 cw_mweb/lib/cw_mweb_platform_interface.dart create mode 100644 cw_mweb/lib/mwebd.pb.dart create mode 100644 cw_mweb/lib/mwebd.pbgrpc.dart create mode 100644 cw_mweb/macos/Classes/CwMwebPlugin.swift create mode 100644 cw_mweb/macos/cw_mweb.podspec create mode 100644 cw_mweb/pubspec.yaml create mode 100644 lib/src/screens/settings/mweb_settings.dart create mode 100644 lib/view_model/settings/mweb_settings_view_model.dart create mode 100755 scripts/android/build_mwebd.sh create mode 100755 scripts/ios/build_mwebd.sh diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index e096545c2..925f4e00d 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -96,6 +96,25 @@ jobs: cd /opt/android/cake_wallet flutter pub get + + - name: Install go and gomobile + run: | + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init + + - name: Build mwebd + run: | + # paths are reset after each step, so we need to set them again: + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + cd /opt/android/cake_wallet/scripts/android/ + ./build_mwebd.sh --dont-install + - name: Generate KeyStore run: | cd /opt/android/cake_wallet/android/app diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 7713cc95d..5ea0cb377 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -89,6 +89,25 @@ jobs: cd /opt/android/cake_wallet flutter pub get + - name: Install go and gomobile + run: | + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init + + - name: Build mwebd + run: | + # paths are reset after each step, so we need to set them again: + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + # build mwebd: + cd /opt/android/cake_wallet/scripts/android/ + ./build_mwebd.sh --dont-install + - name: Generate localization run: | cd /opt/android/cake_wallet diff --git a/assets/images/mweb_logo.png b/assets/images/mweb_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..92317203ed66a67f86f98e22be61bf2102eb44e0 GIT binary patch literal 19409 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 log(x) / log(base); @@ -125,7 +126,7 @@ Future mnemonicToSeedBytes(String mnemonic, return Uint8List.fromList(bytes); } -bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit]).any((el) => el); +bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit, mweb]).any((el) => el); bool validateMnemonic(String mnemonic, {String prefix = segwit}) { try { diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index aa3d4a4cd..8e72bc7b0 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -7,6 +7,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)'); static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); + static const mweb = BitcoinReceivePageOption._('MWEB'); static const silent_payments = BitcoinReceivePageOption._('Silent Payments'); @@ -27,6 +28,11 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinReceivePageOption.p2pkh ]; + static const allLitecoin = [ + BitcoinReceivePageOption.p2wpkh, + BitcoinReceivePageOption.mweb + ]; + BitcoinAddressType toType() { switch (this) { case BitcoinReceivePageOption.p2tr: @@ -39,6 +45,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return P2shAddressType.p2wpkhInP2sh; case BitcoinReceivePageOption.silent_payments: return SilentPaymentsAddresType.p2sp; + case BitcoinReceivePageOption.mweb: + return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: default: return SegwitAddresType.p2wpkh; @@ -51,6 +59,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2tr; case SegwitAddresType.p2wsh: return BitcoinReceivePageOption.p2wsh; + case SegwitAddresType.mweb: + return BitcoinReceivePageOption.mweb; case P2pkhAddressType.p2pkh: return BitcoinReceivePageOption.p2pkh; case P2shAddressType.p2wpkhInP2sh: diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index 7c4dcfd5f..d1f45a545 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -87,7 +87,7 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { } @override - String get units => 'Latoshi'; + String get units => 'Litoshi'; @override String toString() { diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index d8cca5b16..cfcc71d09 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -506,6 +506,12 @@ class ElectrumClient { void _methodHandler({required String method, required Map request}) { switch (method) { + case 'blockchain.headers.subscribe': + final params = request['params'] as List; + final id = 'blockchain.headers.subscribe'; + + _tasks[id]?.subject?.add(params.last); + break; case 'blockchain.scripthash.subscribe': final params = request['params'] as List; final scripthash = params.first as String?; diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 15d6843d8..4e37f40b1 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -7,7 +7,14 @@ class ElectrumBalance extends Balance { required this.confirmed, required this.unconfirmed, required this.frozen, - }) : super(confirmed, unconfirmed); + this.secondConfirmed = 0, + this.secondUnconfirmed = 0, + }) : super( + confirmed, + unconfirmed, + secondAvailable: secondConfirmed, + secondAdditional: secondUnconfirmed, + ); static ElectrumBalance? fromJSON(String? jsonSource) { if (jsonSource == null) { @@ -25,9 +32,12 @@ class ElectrumBalance extends Balance { int confirmed; int unconfirmed; final int frozen; + int secondConfirmed = 0; + int secondUnconfirmed = 0; @override - String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); + String get formattedAvailableBalance => + bitcoinAmountToString(amount: confirmed - frozen); @override String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); @@ -38,6 +48,21 @@ class ElectrumBalance extends Balance { return frozenFormatted == '0.0' ? '' : frozenFormatted; } - String toJSON() => - json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen}); + @override + String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed); + + @override + String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed); + + @override + String get formattedFullAvailableBalance => + bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen); + + String toJSON() => json.encode({ + 'confirmed': confirmed, + 'unconfirmed': unconfirmed, + 'frozen': frozen, + 'secondConfirmed': secondConfirmed, + 'secondUnconfirmed': secondUnconfirmed + }); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index f291247b8..17510e0de 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; -import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -23,6 +23,7 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -112,11 +113,18 @@ abstract class ElectrumWalletBase } if (seedBytes != null) { - return currency == CryptoCurrency.bch - ? bitcoinCashHDWallet(seedBytes) - : Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( + switch (currency) { + case CryptoCurrency.btc: + case CryptoCurrency.ltc: + case CryptoCurrency.tbtc: + return Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)) as Bip32Slip10Secp256k1; + case CryptoCurrency.bch: + return bitcoinCashHDWallet(seedBytes); + default: + throw Exception("Unsupported currency"); + } } return Bip32Slip10Secp256k1.fromExtendedKey(xpub!); @@ -163,11 +171,13 @@ abstract class ElectrumWalletBase Set get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet(); List get scriptHashes => walletAddresses.addressesByReceiveType + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) .map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network)) .toList(); List get publicScriptHashes => walletAddresses.allAddresses .where((addr) => !addr.isHidden) + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) .map((addr) => addr.getScriptHash(network)) .toList(); @@ -274,6 +284,7 @@ abstract class ElectrumWalletBase void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; + StreamSubscription? _receiveStream; Timer? _updateFeeRateTimer; static const int _autoSaveInterval = 1; @@ -324,7 +335,8 @@ abstract class ElectrumWalletBase isSingleScan: doSingleScan ?? false, )); - await for (var message in receivePort) { + _receiveStream?.cancel(); + _receiveStream = receivePort.listen((var message) async { if (message is Map) { for (final map in message.entries) { final txid = map.key; @@ -387,10 +399,16 @@ abstract class ElectrumWalletBase nodeSupportsSilentPayments = false; } - syncStatus = message.syncStatus; + if (message.syncStatus is SyncingSyncStatus) { + var status = message.syncStatus as SyncingSyncStatus; + syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc); + } else { + syncStatus = message.syncStatus; + } + await walletInfo.updateRestoreHeight(message.height); } - } + }); } void _updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) { @@ -430,9 +448,9 @@ abstract class ElectrumWalletBase await _setInitialHeight(); } - await _subscribeForUpdates(); - + await subscribeForUpdates(); await updateTransactions(); + await updateAllUnspents(); await updateBalance(); await updateFeeRates(); @@ -537,6 +555,7 @@ abstract class ElectrumWalletBase try { syncStatus = ConnectingSyncStatus(); + await _receiveStream?.cancel(); await electrumClient.close(); electrumClient.onConnectionStatusChange = _onConnectionStatusChange; @@ -682,26 +701,15 @@ abstract class ElectrumWalletBase paysToSilentPayment: hasSilentPayment, ); - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network as BitcoinCashNetwork, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network, - memo: memo, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - vinOutpoints: utxoDetails.vinOutpoints, - ); - } - - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + int fee = await calcFee( + utxos: utxoDetails.utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, + ); if (fee == 0) { throw BitcoinTransactionNoFeeException(); @@ -788,7 +796,10 @@ abstract class ElectrumWalletBase throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = await walletAddresses.getChangeAddress(); + final changeAddress = await walletAddresses.getChangeAddress( + outputs: outputs, + utxoDetails: utxoDetails, + ); final address = RegexUtils.addressTypeFromStr(changeAddress, network); outputs.add(BitcoinOutput( address: address, @@ -796,26 +807,13 @@ abstract class ElectrumWalletBase isChange: true, )); - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network as BitcoinCashNetwork, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network, - memo: memo, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - vinOutpoints: utxoDetails.vinOutpoints, - ); - } - - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + int fee = await calcFee( + utxos: utxoDetails.utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + ); if (fee == 0) { throw BitcoinTransactionNoFeeException(); @@ -825,6 +823,8 @@ abstract class ElectrumWalletBase final lastOutput = outputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; + print(amountLeftForChangeAndFee); + if (!_isBelowDust(amountLeftForChange)) { // Here, lastOutput already is change, return the amount left without the fee to the user's address. outputs[outputs.length - 1] = BitcoinOutput( @@ -874,7 +874,7 @@ abstract class ElectrumWalletBase final totalAmount = amount + fee; - if (totalAmount > balance[currency]!.confirmed) { + if (totalAmount > (balance[currency]!.confirmed + balance[currency]!.secondConfirmed)) { throw BitcoinTransactionWrongBalanceException(); } @@ -909,6 +909,37 @@ abstract class ElectrumWalletBase ); } + Future calcFee({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate, + List? inputPrivKeyInfos, + List? vinOutpoints, + }) async { + int estimatedSize; + if (network is BitcoinCashNetwork) { + estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + } else { + estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + inputPrivKeyInfos: inputPrivKeyInfos, + vinOutpoints: vinOutpoints, + ); + } + + return feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + } + @override Future createTransaction(Object credentials) async { try { @@ -1134,6 +1165,7 @@ abstract class ElectrumWalletBase 'derivationPath': walletInfo.derivationInfo?.derivationPath, 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), + 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(), }); int feeRate(TransactionPriority priority) { @@ -1253,6 +1285,7 @@ abstract class ElectrumWalletBase @override Future close() async { try { + await _receiveStream?.cancel(); await electrumClient.close(); } catch (_) {} _autoSaveTimer?.cancel(); @@ -1283,71 +1316,55 @@ abstract class ElectrumWalletBase unspentCoins = updatedUnspentCoins; - if (unspentCoinsInfo.isEmpty) { - unspentCoins.forEach((coin) => _addCoinInfo(coin)); + if (unspentCoinsInfo.length != updatedUnspentCoins.length) { + unspentCoins.forEach((coin) => addCoinInfo(coin)); return; } - if (unspentCoins.isNotEmpty) { - unspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where((element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout); - - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; - - coin.isFrozen = coinInfo.isFrozen; - coin.isSending = coinInfo.isSending; - coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; - } else { - _addCoinInfo(coin); - } - }); - } - + await updateCoins(unspentCoins); await _refreshUnspentCoinsInfo(); } - @action - Future updateUnspents(BitcoinAddressRecord address) async { - final newUnspentCoins = await fetchUnspent(address); - - if (newUnspentCoins.isNotEmpty) { - unspentCoins.addAll(newUnspentCoins); - - newUnspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where( - (element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout, - ); - - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; - - coin.isFrozen = coinInfo.isFrozen; - coin.isSending = coinInfo.isSending; - coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; - } else { - _addCoinInfo(coin); - } - }); + Future updateCoins(List newUnspentCoins) async { + if (newUnspentCoins.isEmpty) { + return; } + + newUnspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + addCoinInfo(coin); + } + }); + } + + @action + Future updateUnspentsForAddress(BitcoinAddressRecord address) async { + final newUnspentCoins = await fetchUnspent(address); + await updateCoins(newUnspentCoins); } @action Future> fetchUnspent(BitcoinAddressRecord address) async { - final unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); - + List> unspents = []; List updatedUnspentCoins = []; + unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + await Future.wait(unspents.map((unspent) async { try { final coin = BitcoinUnspent.fromJSON(address, unspent); @@ -1363,7 +1380,7 @@ abstract class ElectrumWalletBase } @action - Future _addCoinInfo(BitcoinUnspent coin) async { + Future addCoinInfo(BitcoinUnspent coin) async { final newInfo = UnspentCoinsInfo( walletId: id, hash: coin.hash, @@ -1714,12 +1731,14 @@ abstract class ElectrumWalletBase final Map historiesWithDetails = {}; if (type == WalletType.bitcoin) { - await Future.wait(ADDRESS_TYPES + await Future.wait(BITCOIN_ADDRESS_TYPES .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.bitcoinCash) { - await fetchTransactionsForAddressType(historiesWithDetails, P2pkhAddressType.p2pkh); + await Future.wait(BITCOIN_CASH_ADDRESS_TYPES + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.litecoin) { - await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh); + await Future.wait(LITECOIN_ADDRESS_TYPES + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } transactionHistory.transactions.values.forEach((tx) async { @@ -1776,7 +1795,7 @@ abstract class ElectrumWalletBase matchedAddresses.toList(), addressRecord.isHidden, (address) async { - await _subscribeForUpdates(); + await subscribeForUpdates(); return _fetchAddressHistory(address, await getCurrentChainTip()) .then((history) => history.isNotEmpty ? address.address : null); }, @@ -1865,7 +1884,7 @@ abstract class ElectrumWalletBase } } - Future _subscribeForUpdates() async { + Future subscribeForUpdates() async { final unsubscribedScriptHashes = walletAddresses.allAddresses.where( (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)), ); @@ -1876,7 +1895,7 @@ abstract class ElectrumWalletBase _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { - await updateUnspents(address); + await updateUnspentsForAddress(address); await updateBalance(); @@ -1893,8 +1912,10 @@ abstract class ElectrumWalletBase })); } - Future _fetchBalances() async { - final addresses = walletAddresses.allAddresses.toList(); + Future fetchBalances() async { + final addresses = walletAddresses.allAddresses + .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) + .toList(); final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; @@ -1944,6 +1965,7 @@ abstract class ElectrumWalletBase totalConfirmed += confirmed; totalUnconfirmed += unconfirmed; + addressRecord.balance = confirmed + unconfirmed; if (confirmed > 0 || unconfirmed > 0) { addressRecord.setAsUsed(); } @@ -1957,22 +1979,10 @@ abstract class ElectrumWalletBase } Future updateBalance() async { - balance[currency] = await _fetchBalances(); + balance[currency] = await fetchBalances(); await save(); } - String getChangeAddress() { - const minCountOfHiddenAddresses = 5; - final random = Random(); - var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList(); - - if (addresses.length < minCountOfHiddenAddresses) { - addresses = walletAddresses.allAddresses.toList(); - } - - return addresses[random.nextInt(addresses.length)].address; - } - @override void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; @@ -2475,6 +2485,8 @@ BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { return SegwitAddresType.p2wsh; } else if (type is P2trAddress) { return SegwitAddresType.p2tr; + } else if (type is MwebAddress) { + return SegwitAddresType.mweb; } else if (type is SilentPaymentsAddresType) { return SilentPaymentsAddresType.p2sp; } else { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index a0424c934..2de86d780 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,6 +1,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -10,7 +11,7 @@ part 'electrum_wallet_addresses.g.dart'; class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; -const List ADDRESS_TYPES = [ +const List BITCOIN_ADDRESS_TYPES = [ SegwitAddresType.p2wpkh, P2pkhAddressType.p2pkh, SegwitAddresType.p2tr, @@ -18,6 +19,15 @@ const List ADDRESS_TYPES = [ P2shAddressType.p2wpkhInP2sh, ]; +const List LITECOIN_ADDRESS_TYPES = [ + SegwitAddresType.p2wpkh, + SegwitAddresType.mweb, +]; + +const List BITCOIN_CASH_ADDRESS_TYPES = [ + P2pkhAddressType.p2pkh, +]; + abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase( WalletInfo walletInfo, { @@ -29,6 +39,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { Map? initialChangeAddressIndex, List? initialSilentAddresses, int initialSilentAddressIndex = 0, + List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), @@ -49,6 +60,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { silentAddresses = ObservableList.of( (initialSilentAddresses ?? []).toSet()), currentSilentAddressIndex = initialSilentAddressIndex, + mwebAddresses = + ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { if (masterHd != null) { silentAddress = SilentPaymentOwner.fromPrivateKeys( @@ -91,6 +104,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final ObservableList receiveAddresses; final ObservableList changeAddresses; final ObservableList silentAddresses; + final ObservableList mwebAddresses; final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; @@ -213,7 +227,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (walletInfo.type == WalletType.bitcoinCash) { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { - await _generateInitialAddresses(); + await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); + await _generateInitialAddresses(type: SegwitAddresType.mweb); } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); @@ -221,6 +236,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await _generateInitialAddresses(type: SegwitAddresType.p2tr); await _generateInitialAddresses(type: SegwitAddresType.p2wsh); } + updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); @@ -237,7 +253,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress() async { + Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -317,12 +333,110 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } - String getAddress( - {required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => + String getAddress({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) => ''; + Future getAddressAsync({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) async => + getAddress(index: index, hd: hd, addressType: addressType); + + void addBitcoinAddressTypes() { + final lastP2wpkh = _addresses + .where((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) + .toList() + .last; + if (lastP2wpkh.address != address) { + addressesMap[lastP2wpkh.address] = 'P2WPKH'; + } else { + addressesMap[address] = 'Active - P2WPKH'; + } + + final lastP2pkh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); + if (lastP2pkh.address != address) { + addressesMap[lastP2pkh.address] = 'P2PKH'; + } else { + addressesMap[address] = 'Active - P2PKH'; + } + + final lastP2sh = _addresses.firstWhere((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh)); + if (lastP2sh.address != address) { + addressesMap[lastP2sh.address] = 'P2SH'; + } else { + addressesMap[address] = 'Active - P2SH'; + } + + final lastP2tr = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); + if (lastP2tr.address != address) { + addressesMap[lastP2tr.address] = 'P2TR'; + } else { + addressesMap[address] = 'Active - P2TR'; + } + + final lastP2wsh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); + if (lastP2wsh.address != address) { + addressesMap[lastP2wsh.address] = 'P2WSH'; + } else { + addressesMap[address] = 'Active - P2WSH'; + } + + silentAddresses.forEach((addressRecord) { + if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { + return; + } + + if (addressRecord.address != address) { + addressesMap[addressRecord.address] = addressRecord.name.isEmpty + ? "Silent Payments" + : "Silent Payments - " + addressRecord.name; + } else { + addressesMap[address] = 'Active - Silent Payments'; + } + }); + } + + void addLitecoinAddressTypes() { + final lastP2wpkh = _addresses + .where((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) + .toList() + .last; + if (lastP2wpkh.address != address) { + addressesMap[lastP2wpkh.address] = 'P2WPKH'; + } else { + addressesMap[address] = 'Active - P2WPKH'; + } + + final lastMweb = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); + if (lastMweb.address != address) { + addressesMap[lastMweb.address] = 'MWEB'; + } else { + addressesMap[address] = 'Active - MWEB'; + } + } + + void addBitcoinCashAddressTypes() { + final lastP2pkh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); + if (lastP2pkh.address != address) { + addressesMap[lastP2pkh.address] = 'P2PKH'; + } else { + addressesMap[address] = 'Active - P2PKH'; + } + } + @override Future updateAddressesInBox() async { try { @@ -334,63 +448,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { allAddressesMap[addressRecord.address] = addressRecord.name; }); - final lastP2wpkh = _addresses - .where((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) - .toList() - .last; - if (lastP2wpkh.address != address) { - addressesMap[lastP2wpkh.address] = 'P2WPKH'; - } else { - addressesMap[address] = 'Active - P2WPKH'; + switch (walletInfo.type) { + case WalletType.bitcoin: + addBitcoinAddressTypes(); + break; + case WalletType.litecoin: + addLitecoinAddressTypes(); + break; + case WalletType.bitcoinCash: + addBitcoinCashAddressTypes(); + break; + default: + break; } - final lastP2pkh = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); - if (lastP2pkh.address != address) { - addressesMap[lastP2pkh.address] = 'P2PKH'; - } else { - addressesMap[address] = 'Active - P2PKH'; - } - - final lastP2sh = _addresses.firstWhere((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh)); - if (lastP2sh.address != address) { - addressesMap[lastP2sh.address] = 'P2SH'; - } else { - addressesMap[address] = 'Active - P2SH'; - } - - final lastP2tr = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); - if (lastP2tr.address != address) { - addressesMap[lastP2tr.address] = 'P2TR'; - } else { - addressesMap[address] = 'Active - P2TR'; - } - - final lastP2wsh = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); - if (lastP2wsh.address != address) { - addressesMap[lastP2wsh.address] = 'P2WSH'; - } else { - addressesMap[address] = 'Active - P2WSH'; - } - - silentAddresses.forEach((addressRecord) { - if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { - return; - } - - if (addressRecord.address != address) { - addressesMap[addressRecord.address] = addressRecord.name.isEmpty - ? "Silent Payments" - : "Silent Payments - " + addressRecord.name; - } else { - addressesMap[address] = 'Active - Silent Payments'; - } - }); - await saveAddressesInBox(); } catch (e) { print(e.toString()); @@ -410,6 +481,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { foundAddress = addressRecord; } }); + mwebAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); if (foundAddress != null) { foundAddress!.setNewName(label); @@ -510,7 +586,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( - getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), + await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), index: i, isHidden: isHidden, type: type ?? addressPageType, @@ -540,15 +616,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); } + @action + void addMwebAddresses(Iterable addresses) { + final addressesSet = this.mwebAddresses.toSet(); + addressesSet.addAll(addresses); + this.mwebAddresses.clear(); + this.mwebAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + void _validateAddresses() { - _addresses.forEach((element) { + _addresses.forEach((element) async { + if (element.type == SegwitAddresType.mweb) { + // this would add a ton of startup lag for mweb addresses since we have 1000 of them + return; + } if (!element.isHidden && element.address != - getAddress(index: element.index, hd: mainHd, addressType: element.type)) { + await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) { element.isHidden = true; } else if (element.isHidden && element.address != - getAddress(index: element.index, hd: sideHd, addressType: element.type)) { + await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) { element.isHidden = false; } }); diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index fa58be238..25cc5637e 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -23,6 +23,7 @@ class ElectrumWalletSnapshot { required this.addressPageType, required this.silentAddresses, required this.silentAddressIndex, + required this.mwebAddresses, this.passphrase, this.derivationType, this.derivationPath, @@ -44,6 +45,8 @@ class ElectrumWalletSnapshot { List addresses; List silentAddresses; + List mwebAddresses; + ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; @@ -56,10 +59,11 @@ class ElectrumWalletSnapshot { final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; - final addressesTmp = data['addresses'] as List? ?? []; final mnemonic = data['mnemonic'] as String?; final xpub = data['xpub'] as String?; final passphrase = data['passphrase'] as String? ?? ''; + + final addressesTmp = data['addresses'] as List? ?? []; final addresses = addressesTmp .whereType() .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) @@ -71,6 +75,12 @@ class ElectrumWalletSnapshot { .map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network)) .toList(); + final mwebAddressTmp = data['mweb_addresses'] as List? ?? []; + final mwebAddresses = mwebAddressTmp + .whereType() + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) + .toList(); + final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; @@ -113,6 +123,7 @@ class ElectrumWalletSnapshot { derivationPath: derivationPath, silentAddresses: silentAddresses, silentAddressIndex: silentAddressIndex, + mwebAddresses: mwebAddresses, ); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 2e4683ae6..d7e6fef61 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,15 +1,31 @@ +import 'dart:async'; import 'dart:convert'; - +import 'package:convert/convert.dart' as convert; +import 'dart:math'; +import 'package:collection/collection.dart'; +import 'package:crypto/crypto.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/mweb_utxo.dart'; +import 'package:cw_mweb/mwebd.pbgrpc.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/signer/ecdsa_signing_key.dart'; -import 'package:bip39/bip39.dart' as bip39; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; @@ -19,8 +35,11 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; +import 'package:grpc/grpc.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_mweb/cw_mweb.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; import 'package:pointycastle/ecc/api.dart'; import 'package:pointycastle/ecc/curves/secp256k1.dart'; @@ -40,34 +59,54 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? passphrase, String? addressPageType, List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + int? initialMwebHeight, + bool? alwaysScan, }) : super( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - network: LitecoinNetwork.mainnet, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - encryptionFileUtils: encryptionFileUtils, - passphrase: passphrase, - currency: CryptoCurrency.ltc) { + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + network: LitecoinNetwork.mainnet, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + encryptionFileUtils: encryptionFileUtils, + currency: CryptoCurrency.ltc, + ) { + mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; + mwebEnabled = alwaysScan ?? false; walletAddresses = LitecoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, + initialMwebAddresses: initialMwebAddresses, mainHd: hd, sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, + mwebHd: mwebHd, + mwebEnabled: mwebEnabled, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } + late final Bip32Slip10Secp256k1 mwebHd; + late final Box mwebUtxosBox; + Timer? _syncTimer; + Timer? _feeRatesTimer; + Timer? _processingTimer; + StreamSubscription? _utxoStream; + late RpcClient _stub; + late bool mwebEnabled; + bool processingUtxos = false; + + List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; static Future create( {required String mnemonic, @@ -78,6 +117,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? passphrase, String? addressPageType, List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex}) async { @@ -101,6 +141,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, + initialMwebAddresses: initialMwebAddresses, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, passphrase: passphrase, @@ -111,12 +152,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } - static Future open( - {required String name, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required String password, - required EncryptionFileUtils encryptionFileUtils}) async { + static Future open({ + required String name, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required String password, + required bool alwaysScan, + required EncryptionFileUtils encryptionFileUtils, + }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); ElectrumWalletSnapshot? snp = null; @@ -178,6 +221,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp?.addresses, + initialMwebAddresses: snp?.mwebAddresses, initialBalance: snp?.balance, seedBytes: seedBytes!, passphrase: passphrase, @@ -185,6 +229,551 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, + alwaysScan: alwaysScan, + ); + } + + Future waitForMwebAddresses() async { + // ensure that we have the full 1000 mweb addresses generated before continuing: + // should no longer be needed, but leaving here just in case + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + while (mwebAddrs.length < 1000) { + print("waiting for mweb addresses to finish generating..."); + await Future.delayed(const Duration(milliseconds: 1000)); + } + } + + @action + @override + Future startSync() async { + if (syncStatus is SyncronizingSyncStatus) { + return; + } + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + try { + syncStatus = SyncronizingSyncStatus(); + await subscribeForUpdates(); + updateFeeRates(); + + _feeRatesTimer?.cancel(); + _feeRatesTimer = + Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); + + if (!mwebEnabled) { + try { + await updateAllUnspents(); + await updateTransactions(); + await updateBalance(); + syncStatus = SyncedSyncStatus(); + } catch (e, s) { + print(e); + print(s); + syncStatus = FailedSyncStatus(); + } + return; + } + + await waitForMwebAddresses(); + await getStub(); + await processMwebUtxos(); + await updateTransactions(); + await updateUnspent(); + await updateBalance(); + } catch (e) { + print("failed to start mweb sync: $e"); + syncStatus = FailedSyncStatus(); + return; + } + + _syncTimer?.cancel(); + _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { + if (syncStatus is FailedSyncStatus) return; + + final nodeHeight = + await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node + final resp = await CwMweb.status(StatusRequest()); + print("resp.mwebUtxosHeight: ${resp.mwebUtxosHeight}"); + print("resp.mwebHeaderHeight: ${resp.mwebHeaderHeight}"); + print("resp.blockHeaderHeight: ${resp.blockHeaderHeight}"); + + if (resp.blockHeaderHeight < nodeHeight) { + int h = resp.blockHeaderHeight; + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebHeaderHeight < nodeHeight) { + int h = resp.mwebHeaderHeight; + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebUtxosHeight < nodeHeight) { + syncStatus = SyncingSyncStatus(1, 0.999); + } else { + if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { + await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); + await checkMwebUtxosSpent(); + // update the confirmations for each transaction: + for (final transaction in transactionHistory.transactions.values) { + if (transaction.isPending) continue; + int txHeight = transaction.height ?? resp.mwebUtxosHeight; + final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; + if (transaction.confirmations == confirmations) continue; + transaction.confirmations = confirmations; + transactionHistory.addOne(transaction); + } + await transactionHistory.save(); + } + + // prevent unnecessary reaction triggers: + if (syncStatus is! SyncedSyncStatus) { + // mwebd is synced, but we could still be processing incoming utxos: + if (!processingUtxos) { + syncStatus = SyncedSyncStatus(); + } + } + return; + } + }); + } + + @action + @override + Future stopSync() async { + _syncTimer?.cancel(); + _utxoStream?.cancel(); + _feeRatesTimer?.cancel(); + await CwMweb.stop(); + } + + Future initMwebUtxosBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}"; + + mwebUtxosBox = await CakeHive.openBox(boxName); + } + + @override + Future renameWalletFiles(String newWalletName) async { + // rename the hive box: + final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}"; + final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}"; + + final oldBox = await CakeHive.openBox(oldBoxName); + mwebUtxosBox = await CakeHive.openBox(newBoxName); + for (final key in oldBox.keys) { + await mwebUtxosBox.put(key, oldBox.get(key)!); + } + oldBox.deleteFromDisk(); + + await super.renameWalletFiles(newWalletName); + } + + @action + @override + Future rescan({ + required int height, + int? chainTip, + ScanData? scanData, + bool? doSingleScan, + bool? usingElectrs, + }) async { + _syncTimer?.cancel(); + int oldHeight = walletInfo.restoreHeight; + await walletInfo.updateRestoreHeight(height); + + // go through mwebUtxos and clear any that are above the new restore height: + if (height == 0) { + await mwebUtxosBox.clear(); + transactionHistory.clear(); + } else { + for (final utxo in mwebUtxosBox.values) { + if (utxo.height > height) { + await mwebUtxosBox.delete(utxo.outputId); + } + } + // TODO: remove transactions that are above the new restore height! + } + + // reset coin balances and txCount to 0: + unspentCoins.forEach((coin) { + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance = 0; + coin.bitcoinAddressRecord.txCount = 0; + }); + + for (var addressRecord in walletAddresses.allAddresses) { + addressRecord.balance = 0; + addressRecord.txCount = 0; + } + + await startSync(); + } + + @override + Future init() async { + await super.init(); + await initMwebUtxosBox(); + } + + Future handleIncoming(MwebUtxo utxo, RpcClient stub) async { + final status = await stub.status(StatusRequest()); + var date = DateTime.now(); + var confirmations = 0; + if (utxo.height > 0) { + date = DateTime.fromMillisecondsSinceEpoch(utxo.blockTime * 1000); + confirmations = status.blockHeaderHeight - utxo.height + 1; + } + var tx = transactionHistory.transactions.values + .firstWhereOrNull((tx) => tx.outputAddresses?.contains(utxo.outputId) ?? false); + + if (tx == null) { + tx = ElectrumTransactionInfo( + WalletType.litecoin, + id: utxo.outputId, + height: utxo.height, + amount: utxo.value.toInt(), + fee: 0, + direction: TransactionDirection.incoming, + isPending: utxo.height == 0, + date: date, + confirmations: confirmations, + inputAddresses: [], + outputAddresses: [utxo.outputId], + isReplaced: false, + ); + } + + // don't update the confirmations if the tx is updated by electrum: + if (tx.confirmations == 0 || utxo.height != 0) { + tx.height = utxo.height; + tx.isPending = utxo.height == 0; + tx.confirmations = confirmations; + } + + bool isNew = transactionHistory.transactions[tx.id] == null; + + if (!(tx.outputAddresses?.contains(utxo.address) ?? false)) { + tx.outputAddresses?.add(utxo.address); + isNew = true; + } + + if (isNew) { + final addressRecord = walletAddresses.allAddresses + .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address); + if (addressRecord == null) { + print("we don't have this address in the wallet! ${utxo.address}"); + return; + } + + // update the txCount: + addressRecord.txCount++; + addressRecord.balance += utxo.value.toInt(); + addressRecord.setAsUsed(); + } + + transactionHistory.addOne(tx); + + if (isNew) { + // update the unconfirmed balance when a new tx is added: + // we do this after adding the tx to the history so that sub address balances are updated correctly + // (since that calculation is based on the tx history) + await updateBalance(); + } + } + + Future processMwebUtxos() async { + if (!mwebEnabled) { + return; + } + + int restoreHeight = walletInfo.restoreHeight; + print("SCANNING FROM HEIGHT: $restoreHeight"); + final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight); + + // process new utxos as they come in: + _utxoStream?.cancel(); + ResponseStream? responseStream = await CwMweb.utxos(req); + if (responseStream == null) { + throw Exception("failed to get utxos stream!"); + } + _utxoStream = responseStream.listen((Utxo sUtxo) async { + // we're processing utxos, so our balance could still be innacurate: + if (syncStatus is! SyncronizingSyncStatus && syncStatus is! SyncingSyncStatus) { + syncStatus = SyncronizingSyncStatus(); + processingUtxos = true; + _processingTimer?.cancel(); + _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { + processingUtxos = false; + timer.cancel(); + }); + } + + final utxo = MwebUtxo( + address: sUtxo.address, + blockTime: sUtxo.blockTime, + height: sUtxo.height, + outputId: sUtxo.outputId, + value: sUtxo.value.toInt(), + ); + + // if (mwebUtxosBox.containsKey(utxo.outputId)) { + // // we've already stored this utxo, skip it: + // return; + // } + + await updateUnspent(); + await updateBalance(); + + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + + // don't process utxos with addresses that are not in the mwebAddrs list: + if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) { + return; + } + + await mwebUtxosBox.put(utxo.outputId, utxo); + + await handleIncoming(utxo, _stub); + }); + } + + Future checkMwebUtxosSpent() async { + if (!mwebEnabled) { + return; + } + + final pendingOutgoingTransactions = transactionHistory.transactions.values + .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending); + + // check if any of the pending outgoing transactions are now confirmed: + bool updatedAny = false; + for (final tx in pendingOutgoingTransactions) { + updatedAny = await isConfirmed(tx) || updatedAny; + } + + // get output ids of all the mweb utxos that have > 0 height: + final outputIds = + mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList(); + + final resp = await CwMweb.spent(SpentRequest(outputId: outputIds)); + final spent = resp.outputId; + if (spent.isEmpty) { + return; + } + + final status = await CwMweb.status(StatusRequest()); + final height = await electrumClient.getCurrentBlockChainTip(); + if (height == null || status.blockHeaderHeight != height) return; + if (status.mwebUtxosHeight != height) return; // we aren't synced + + int amount = 0; + Set inputAddresses = {}; + var output = convert.AccumulatorSink(); + var input = sha256.startChunkedConversion(output); + + for (final outputId in spent) { + final utxo = mwebUtxosBox.get(outputId); + await mwebUtxosBox.delete(outputId); + if (utxo == null) continue; + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + if (!inputAddresses.contains(utxo.address)) { + addressRecord.txCount++; + } + addressRecord.balance -= utxo.value.toInt(); + amount += utxo.value.toInt(); + inputAddresses.add(utxo.address); + input.add(hex.decode(outputId)); + } + + if (inputAddresses.isEmpty) return; + input.close(); + var digest = output.events.single; + final tx = ElectrumTransactionInfo( + WalletType.litecoin, + id: digest.toString(), + height: height, + amount: amount, + fee: 0, + direction: TransactionDirection.outgoing, + isPending: false, + date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000), + confirmations: 1, + inputAddresses: inputAddresses.toList(), + outputAddresses: [], + isReplaced: false, + ); + + transactionHistory.addOne(tx); + await transactionHistory.save(); + + if (updatedAny) { + await updateBalance(); + } + } + + // checks if a pending transaction is now confirmed, and updates the tx info accordingly: + Future isConfirmed(ElectrumTransactionInfo tx) async { + if (!mwebEnabled) return false; + if (!tx.isPending) return false; + + final outputId = [], target = {}; + final isHash = RegExp(r'^[a-f0-9]{64}$').hasMatch; + final spendingOutputIds = tx.inputAddresses?.where(isHash) ?? []; + final payingToOutputIds = tx.outputAddresses?.where(isHash) ?? []; + outputId.addAll(spendingOutputIds); + outputId.addAll(payingToOutputIds); + target.addAll(spendingOutputIds); + + for (final outputId in payingToOutputIds) { + final spendingTx = transactionHistory.transactions.values + .firstWhereOrNull((tx) => tx.inputAddresses?.contains(outputId) ?? false); + if (spendingTx != null && !spendingTx.isPending) { + target.add(outputId); + } + } + + if (outputId.isEmpty) { + return false; + } + + final resp = await CwMweb.spent(SpentRequest(outputId: outputId)); + if (!setEquals(resp.outputId.toSet(), target)) { + return false; + } + + final status = await CwMweb.status(StatusRequest()); + tx.height = status.mwebUtxosHeight; + tx.confirmations = 1; + tx.isPending = false; + await transactionHistory.save(); + return true; + } + + Future updateUnspent() async { + await checkMwebUtxosSpent(); + await updateAllUnspents(); + } + + @override + @action + Future updateAllUnspents() async { + // get ltc unspents: + await super.updateAllUnspents(); + + if (!mwebEnabled) { + return; + } + await getStub(); + + // add the mweb unspents to the list: + List mwebUnspentCoins = []; + // update mweb unspents: + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + mwebUtxosBox.keys.forEach((dynamic oId) { + final String outputId = oId as String; + final utxo = mwebUtxosBox.get(outputId); + if (utxo == null) { + return; + } + if (utxo.address.isEmpty) { + // not sure if a bug or a special case but we definitely ignore these + return; + } + final addressRecord = walletAddresses.allAddresses + .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address); + + if (addressRecord == null) { + print("utxo contains an address that is not in the wallet: ${utxo.address}"); + return; + } + final unspent = BitcoinUnspent( + addressRecord, + outputId, + utxo.value.toInt(), + mwebAddrs.indexOf(utxo.address), + ); + if (unspent.vout == 0) { + unspent.isChange = true; + } + mwebUnspentCoins.add(unspent); + }); + unspentCoins.addAll(mwebUnspentCoins); + } + + @override + Future fetchBalances() async { + final balance = await super.fetchBalances(); + if (!mwebEnabled) { + return balance; + } + await getStub(); + + // update unspent balances: + await updateUnspent(); + + int confirmed = balance.confirmed; + int unconfirmed = balance.unconfirmed; + int confirmedMweb = 0; + int unconfirmedMweb = 0; + try { + mwebUtxosBox.values.forEach((utxo) { + if (utxo.height > 0) { + confirmedMweb += utxo.value.toInt(); + } else { + unconfirmedMweb += utxo.value.toInt(); + } + }); + if (unconfirmedMweb > 0) { + unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb); + } + } catch (_) {} + + for (var addressRecord in walletAddresses.allAddresses) { + addressRecord.balance = 0; + addressRecord.txCount = 0; + } + + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + super.addCoinInfo(coin); + } + }); + + // update the txCount for each address using the tx history, since we can't rely on mwebd + // to have an accurate count, we should just keep it in sync with what we know from the tx history: + for (final tx in transactionHistory.transactions.values) { + // if (tx.isPending) continue; + if (tx.inputAddresses == null || tx.outputAddresses == null) { + continue; + } + final txAddresses = tx.inputAddresses! + tx.outputAddresses!; + for (final address in txAddresses) { + final addressRecord = walletAddresses.allAddresses + .firstWhereOrNull((addressRecord) => addressRecord.address == address); + if (addressRecord == null) { + continue; + } + addressRecord.txCount++; + } + } + + return ElectrumBalance( + confirmed: confirmed, + unconfirmed: unconfirmed, + frozen: balance.frozen, + secondConfirmed: confirmedMweb, + secondUnconfirmed: unconfirmedMweb, ); } @@ -204,6 +793,227 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return 0; } + @override + Future calcFee({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate, + List? inputPrivKeyInfos, + List? vinOutpoints, + }) async { + final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); + final paysToMweb = outputs + .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); + if (!spendsMweb && !paysToMweb) { + return await super.calcFee( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: inputPrivKeyInfos, + vinOutpoints: vinOutpoints, + ); + } + + if (!mwebEnabled) { + throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!"); + } + + if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) { + outputs = [ + BitcoinScriptOutput( + script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue()) + ]; + } + + // https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation + final preOutputSum = + outputs.fold(BigInt.zero, (acc, output) => acc + output.toOutput.amount); + final fee = utxos.sumOfUtxosValue() - preOutputSum; + final txb = + BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network); + final resp = await CwMweb.create(CreateRequest( + rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(), + scanSecret: scanSecret, + spendSecret: spendSecret, + feeRatePerKb: Int64(feeRate * 1000), + dryRun: true)); + final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + final posUtxos = utxos + .where((utxo) => tx.inputs + .any((input) => input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout)) + .toList(); + final posOutputSum = tx.outputs.fold(0, (acc, output) => acc + output.amount.toInt()); + final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue(); + final expectedPegin = max(0, (preOutputSum - mwebInputSum).toInt()); + var feeIncrease = posOutputSum - expectedPegin; + if (expectedPegin > 0 && fee == BigInt.zero) { + feeIncrease += await super.calcFee( + utxos: posUtxos, + outputs: tx.outputs + .map((output) => + BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)) + .toList(), + network: network, + memo: memo, + feeRate: feeRate) + + feeRate * 41; + } + return fee.toInt() + feeIncrease; + } + + @override + Future createTransaction(Object credentials) async { + try { + var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction; + tx.isMweb = mwebEnabled; + + if (!mwebEnabled) { + return tx; + } + await waitForMwebAddresses(); + await getStub(); + + final resp = await CwMweb.create(CreateRequest( + rawTx: hex.decode(tx.hex), + scanSecret: scanSecret, + spendSecret: spendSecret, + feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000, + )); + final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + + // check if the transaction doesn't contain any mweb inputs or outputs: + final transactionCredentials = credentials as BitcoinTransactionCredentials; + + bool hasMwebInput = false; + bool hasMwebOutput = false; + + for (final output in transactionCredentials.outputs) { + if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) { + hasMwebOutput = true; + break; + } + } + + if (tx2.mwebBytes != null && tx2.mwebBytes!.isNotEmpty) { + hasMwebInput = true; + } + + if (!hasMwebInput && !hasMwebOutput) { + return tx; + } + + // check if any of the inputs of this transaction are hog-ex: + // this list is only non-mweb inputs: + tx2.inputs.forEach((txInput) { + bool isHogEx = true; + + final utxo = unspentCoins + .firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex); + + // TODO: detect actual hog-ex inputs + + if (!isHogEx) { + return; + } + + int confirmations = utxo.confirmations ?? 0; + if (confirmations < 6) { + throw Exception( + "A transaction input has less than 6 confirmations, please try again later."); + } + }); + + tx.hexOverride = tx2 + .copyWith( + witnesses: tx2.inputs.asMap().entries.map((e) { + final utxo = unspentCoins + .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); + final key = generateECPrivate( + hd: utxo.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utxo.bitcoinAddressRecord.index, + network: network); + final digest = tx2.getTransactionSegwitDigit( + txInIndex: e.key, + script: key.getPublic().toP2pkhAddress().toScriptPubKey(), + amount: BigInt.from(utxo.value), + ); + return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]); + }).toList()) + .toHex(); + tx.outputAddresses = resp.outputId; + + return tx + ..addListener((transaction) async { + final addresses = {}; + transaction.inputAddresses?.forEach((id) async { + final utxo = mwebUtxosBox.get(id); + // await mwebUtxosBox.delete(id);// gets deleted in checkMwebUtxosSpent + if (utxo == null) return; + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + if (!addresses.contains(utxo.address)) { + addresses.add(utxo.address); + } + addressRecord.balance -= utxo.value.toInt(); + }); + transaction.inputAddresses?.addAll(addresses); + + transactionHistory.addOne(transaction); + await updateUnspent(); + await updateBalance(); + }); + } catch (e, s) { + print(e); + print(s); + if (e.toString().contains("commit failed")) { + throw Exception("Transaction commit failed (no peers responded), please try again."); + } + rethrow; + } + } + + @override + Future save() async { + await super.save(); + } + + @override + Future close() async { + _utxoStream?.cancel(); + _feeRatesTimer?.cancel(); + _syncTimer?.cancel(); + _processingTimer?.cancel(); + await stopSync(); + await super.close(); + } + + Future setMwebEnabled(bool enabled) async { + if (mwebEnabled == enabled) { + return; + } + + mwebEnabled = enabled; + (walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled; + await stopSync(); + await startSync(); + } + + Future getStub() async { + _stub = await CwMweb.stub(); + return _stub; + } + + Future getStatusRequest() async { + final resp = await CwMweb.status(StatusRequest()); + return resp; + } + @override Future signMessage(String message, {String? address = null}) async { final index = address != null diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 6945db445..3a7856516 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,8 +1,15 @@ +import 'dart:async'; +import 'dart:typed_data'; + import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; @@ -15,15 +22,145 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required super.mainHd, required super.sideHd, required super.network, + required this.mwebHd, + required this.mwebEnabled, super.initialAddresses, + super.initialMwebAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, - }) : super(walletInfo); + }) : super(walletInfo) { + for (int i = 0; i < mwebAddresses.length; i++) { + mwebAddrs.add(mwebAddresses[i].address); + } + print("initialized with ${mwebAddrs.length} mweb addresses"); + } + + final Bip32Slip10Secp256k1 mwebHd; + bool mwebEnabled; + int mwebTopUpIndex = 1000; + List mwebAddrs = []; + + List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendPubkey => + mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @override - String getAddress( - {required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => - generateP2WPKHAddress(hd: hd, index: index, network: network); + Future init() async { + await initMwebAddresses(); + await super.init(); + } + + @computed + @override + List get allAddresses { + return List.from(super.allAddresses)..addAll(mwebAddresses); + } + + Future ensureMwebAddressUpToIndexExists(int index) async { + Uint8List scan = Uint8List.fromList(scanSecret); + Uint8List spend = Uint8List.fromList(spendPubkey); + int count = 0; + while (mwebAddrs.length <= (index + 1)) { + final address = await CwMweb.address(scan, spend, mwebAddrs.length); + mwebAddrs.add(address!); + count++; + // sleep for a bit to avoid making the main thread unresponsive: + if (count > 50) { + count = 0; + await Future.delayed(Duration(milliseconds: 100)); + } + } + } + + Future initMwebAddresses() async { + if (mwebAddrs.length < 1000) { + print("Generating MWEB addresses..."); + await ensureMwebAddressUpToIndexExists(1020); + print("done generating MWEB addresses"); + List addressRecords = mwebAddrs + .asMap() + .entries + .map((e) => BitcoinAddressRecord( + e.value, + index: e.key, + type: SegwitAddresType.mweb, + network: network, + )) + .toList(); + addMwebAddresses(addressRecords); + print("added ${addressRecords.length} mweb addresses"); + return; + } + } + + @override + String getAddress({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) { + if (addressType == SegwitAddresType.mweb) { + return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1]; + } + return generateP2WPKHAddress(hd: hd, index: index, network: network); + } + + @override + Future getAddressAsync({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) async { + if (addressType == SegwitAddresType.mweb) { + await ensureMwebAddressUpToIndexExists(index); + } + return getAddress(index: index, hd: hd, addressType: addressType); + } + + @action + @override + Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + // use regular change address on peg in, otherwise use mweb for change address: + + if (!mwebEnabled) { + return super.getChangeAddress(); + } + + if (outputs != null && utxoDetails != null) { + // check if this is a PEGIN: + bool outputsToMweb = false; + bool comesFromMweb = false; + + for (var i = 0; i < outputs.length; i++) { + // TODO: probably not the best way to tell if this is an mweb address + // (but it doesn't contain the "mweb" text at this stage) + if (outputs[i].address.toAddress(network).length > 110) { + outputsToMweb = true; + } + } + // TODO: this doesn't respect coin control because it doesn't know which available inputs are selected + utxoDetails.availableInputs.forEach((element) { + if (element.address.contains("mweb")) { + comesFromMweb = true; + } + }); + + bool isPegIn = !comesFromMweb && outputsToMweb; + if (isPegIn && mwebEnabled) { + return super.getChangeAddress(); + } + + // use regular change address if it's not an mweb tx: + if (!comesFromMweb && !outputsToMweb) { + return super.getChangeAddress(); + } + } + + if (mwebEnabled) { + await ensureMwebAddressUpToIndexExists(1); + return mwebAddrs[0]; + } + + return super.getChangeAddress(); + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index e42669402..c659dd658 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; +import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_core/wallet_service.dart'; @@ -14,16 +14,19 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:collection/collection.dart'; import 'package:bip39/bip39.dart' as bip39; +import 'package:path_provider/path_provider.dart'; class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, BitcoinNewWalletCredentials> { - LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect); + LitecoinWalletService( + this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; + final bool alwaysScan; final bool isDirect; @override @@ -64,6 +67,7 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; @@ -73,6 +77,7 @@ class LitecoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -85,6 +90,7 @@ class LitecoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -98,6 +104,23 @@ class LitecoinWalletService extends WalletService< final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); + + // if there are no more litecoin wallets left, cleanup the neutrino db and other files created by mwebd: + if (walletInfoSource.values.where((info) => info.type == WalletType.litecoin).isEmpty) { + final appDirPath = (await getApplicationSupportDirectory()).path; + File neturinoDb = File('$appDirPath/neutrino.db'); + File blockHeaders = File('$appDirPath/block_headers.bin'); + File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin'); + if (neturinoDb.existsSync()) { + neturinoDb.deleteSync(); + } + if (blockHeaders.existsSync()) { + blockHeaders.deleteSync(); + } + if (regFilterHeaders.existsSync()) { + regFilterHeaders.deleteSync(); + } + } } @override @@ -109,6 +132,7 @@ class LitecoinWalletService extends WalletService< name: currentName, walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index aeba9394a..30cc29ec9 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,11 +1,15 @@ +import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:cw_mweb/mwebd.pb.dart'; class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction( @@ -19,6 +23,7 @@ class PendingBitcoinTransaction with PendingTransaction { required this.hasChange, this.isSendAll = false, this.hasTaprootInputs = false, + this.isMweb = false, }) : _listeners = []; final WalletType type; @@ -28,15 +33,19 @@ class PendingBitcoinTransaction with PendingTransaction { final int fee; final String feeRate; final BasedUtxoNetwork? network; - final bool hasChange; final bool isSendAll; + final bool hasChange; final bool hasTaprootInputs; + bool isMweb; + String? idOverride; + String? hexOverride; + List? outputAddresses; @override - String get id => _tx.txId(); + String get id => idOverride ?? _tx.txId(); @override - String get hex => _tx.serialize(); + String get hex => hexOverride ?? _tx.serialize(); @override String get amountFormatted => bitcoinAmountToString(amount: amount); @@ -62,8 +71,7 @@ class PendingBitcoinTransaction with PendingTransaction { final List _listeners; - @override - Future commit() async { + Future _commit() async { int? callId; final result = await electrumClient.broadcastTransaction( @@ -100,6 +108,25 @@ class PendingBitcoinTransaction with PendingTransaction { throw BitcoinTransactionCommitFailed(); } + } + + Future _ltcCommit() async { + try { + final stub = await CwMweb.stub(); + final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); + idOverride = resp.txid; + } on GrpcError catch (e) { + throw BitcoinTransactionCommitFailed(errorMessage: e.message); + } + } + + @override + Future commit() async { + if (isMweb) { + await _ltcCommit(); + } else { + await _commit(); + } _listeners.forEach((listener) => listener(transactionInfo())); } @@ -116,5 +143,7 @@ class PendingBitcoinTransaction with PendingTransaction { isPending: true, isReplaced: false, confirmations: 0, + inputAddresses: _tx.inputs.map((input) => input.txId).toList(), + outputAddresses: outputAddresses, fee: fee); } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index c46260cee..f741230a5 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.0" + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" args: dependency: transitive description: @@ -260,6 +268,13 @@ packages: relative: true source: path version: "0.0.1" + cw_mweb: + dependency: "direct main" + description: + path: "../cw_mweb" + relative: true + source: path + version: "0.0.1" dart_style: dependency: transitive description: @@ -379,6 +394,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da + url: "https://pub.dev" + source: hosted + version: "1.4.1" graphs: dependency: transitive description: @@ -387,6 +410,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + grpc: + dependency: "direct main" + description: + name: grpc + sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 + url: "https://pub.dev" + source: hosted + version: "3.2.4" hex: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index d3fe2ee4e..7e33d8260 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -34,11 +34,16 @@ dependencies: ledger_bitcoin: git: url: https://github.com/cake-tech/ledger-bitcoin + cw_mweb: + path: ../cw_mweb + grpc: ^3.2.4 sp_scanner: git: url: https://github.com/cake-tech/sp_scanner ref: sp_v4.0.0 - + bech32: + git: + url: https://github.com/cake-tech/bech32.git dev_dependencies: flutter_test: @@ -54,10 +59,13 @@ dependency_overrides: url: https://github.com/cake-tech/ledger-flutter.git ref: cake-v3 watcher: ^1.1.0 + protobuf: ^3.1.0 bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v7 + ref: cake-update-v8 + pointycastle: 3.7.4 + ffi: 2.1.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index a3b113d9f..cd1e52f51 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -42,7 +42,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v7 + ref: cake-update-v8 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index 431aff515..7350c80f1 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -1,13 +1,17 @@ abstract class Balance { - const Balance(this.available, this.additional); + const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional}); final int available; final int additional; + final int? secondAvailable; + final int? secondAdditional; + String get formattedAvailableBalance; - String get formattedAdditionalBalance; - String get formattedUnAvailableBalance => ''; + String get formattedSecondAvailableBalance => ''; + String get formattedSecondAdditionalBalance => ''; + String get formattedFullAvailableBalance => formattedAvailableBalance; } diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 10a2aeab5..2b0b77a89 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -310,6 +310,11 @@ DateTime getDateByBitcoinHeight(int height) { return estimatedDate; } +int getLtcHeightByDate({required DateTime date}) { + // TODO: use the proxy layer to get the height with a binary search of blocked header heights + return 0; +} + // TODO: enhance all of this global const lists const wowDates = { "2023-12": 583048, diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index 4da616a79..6432c484b 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -18,3 +18,4 @@ const SPL_TOKEN_TYPE_ID = 16; const DERIVATION_INFO_TYPE_ID = 17; const TRON_TOKEN_TYPE_ID = 18; const HARDWARE_WALLET_TYPE_TYPE_ID = 19; +const MWEB_UTXO_TYPE_ID = 20; \ No newline at end of file diff --git a/cw_core/lib/mweb_utxo.dart b/cw_core/lib/mweb_utxo.dart new file mode 100644 index 000000000..f8dfab395 --- /dev/null +++ b/cw_core/lib/mweb_utxo.dart @@ -0,0 +1,33 @@ +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'mweb_utxo.g.dart'; + +@HiveType(typeId: MWEB_UTXO_TYPE_ID) +class MwebUtxo extends HiveObject { + MwebUtxo({ + required this.height, + required this.value, + required this.address, + required this.outputId, + required this.blockTime, + }); + + static const typeId = MWEB_UTXO_TYPE_ID; + static const boxName = 'MwebUtxo'; + + @HiveField(0) + int height; + + @HiveField(1) + int value; + + @HiveField(2) + String address; + + @HiveField(3) + String outputId; + + @HiveField(4) + int blockTime; +} diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 788309d8c..627b513b2 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -89,4 +89,4 @@ class TimedOutSyncStatus extends NotConnectedSyncStatus { class LostConnectionSyncStatus extends NotConnectedSyncStatus { @override String toString() => 'Reconnecting'; -} +} \ No newline at end of file diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 48ea90f7c..4df1aa1f9 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -67,6 +67,8 @@ abstract class WalletBase startSync(); + Future stopSync() async {} + Future createTransaction(Object credentials); int calculateEstimatedFee(TransactionPriority priority, int? amount); diff --git a/cw_haven/android/build.gradle b/cw_haven/android/build.gradle index 87e8df641..d29c31d4e 100644 --- a/cw_haven/android/build.gradle +++ b/cw_haven/android/build.gradle @@ -35,7 +35,6 @@ android { } externalNativeBuild { cmake { - path "CMakeLists.txt" } } } diff --git a/cw_mweb/.gitignore b/cw_mweb/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_mweb/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_mweb/.metadata b/cw_mweb/.metadata new file mode 100644 index 000000000..606303914 --- /dev/null +++ b/cw_mweb/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: macos + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_mweb/CHANGELOG.md b/cw_mweb/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_mweb/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_mweb/LICENSE b/cw_mweb/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_mweb/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_mweb/README.md b/cw_mweb/README.md new file mode 100644 index 000000000..8a839b1ec --- /dev/null +++ b/cw_mweb/README.md @@ -0,0 +1,15 @@ +# cw_mweb + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/cw_mweb/analysis_options.yaml b/cw_mweb/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_mweb/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_mweb/android/.gitignore b/cw_mweb/android/.gitignore new file mode 100644 index 000000000..881f3d95c --- /dev/null +++ b/cw_mweb/android/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +/libs +.cxx diff --git a/cw_mweb/android/build.gradle b/cw_mweb/android/build.gradle new file mode 100644 index 000000000..7e67b98ad --- /dev/null +++ b/cw_mweb/android/build.gradle @@ -0,0 +1,76 @@ +group 'com.cakewallet.mweb' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.allprojects { + repositories { + flatDir { + dirs project(':cw_mweb').file('libs') + } + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } + + dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.0.0' + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + implementation (name: 'mwebd', ext: 'aar') +} diff --git a/cw_mweb/android/settings.gradle b/cw_mweb/android/settings.gradle new file mode 100644 index 000000000..88fbd66fb --- /dev/null +++ b/cw_mweb/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cw_mweb' diff --git a/cw_mweb/android/src/main/AndroidManifest.xml b/cw_mweb/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..fd3746a8c --- /dev/null +++ b/cw_mweb/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt new file mode 100644 index 000000000..57ae3d4c3 --- /dev/null +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -0,0 +1,58 @@ +package com.cakewallet.mweb + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +import mwebd.Mwebd +import mwebd.Server + +/** CwMwebPlugin */ +class CwMwebPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + private var server: Server? = null + private var port: Long? = null + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "start") { + server?.stop() + val dataDir = call.argument("dataDir") ?: "" + server = server ?: Mwebd.newServer("", dataDir, "") + port = server?.start(0) + result.success(port) + } else if (call.method == "stop") { + server?.stop() + server = null + port = null + result.success(null) + } else if (call.method == "address") { + val scanSecret: ByteArray = call.argument("scanSecret") ?: ByteArray(0) + val spendPub: ByteArray = call.argument("spendPub") ?: ByteArray(0) + val index: Int = call.argument("index") ?: 0 + val res = Mwebd.address(scanSecret, spendPub, index) + result.success(res) + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + server?.stop() + server = null + port = null + } +} diff --git a/cw_mweb/ios/.gitignore b/cw_mweb/ios/.gitignore new file mode 100644 index 000000000..0c885071e --- /dev/null +++ b/cw_mweb/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_mweb/ios/Assets/.gitkeep b/cw_mweb/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cw_mweb/ios/Classes/CwMwebPlugin.swift b/cw_mweb/ios/Classes/CwMwebPlugin.swift new file mode 100644 index 000000000..f1fd78cd8 --- /dev/null +++ b/cw_mweb/ios/Classes/CwMwebPlugin.swift @@ -0,0 +1,86 @@ +import Flutter +import UIKit +import Mwebd + +public class CwMwebPlugin: NSObject, FlutterPlugin { +public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger()) + let instance = CwMwebPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + private static var server: MwebdServer? + private static var port: Int = 0 + private static var dataDir: String? + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + break + case "start": + stopServer() + let args = call.arguments as? [String: String] + let dataDir = args?["dataDir"] + CwMwebPlugin.dataDir = dataDir + startServer(result: result) + break + case "stop": + stopServer() + result(nil) + break + case "address": + let args = call.arguments as! [String: Any] + let scanSecret = args["scanSecret"] as! FlutterStandardTypedData + let spendPub = args["spendPub"] as! FlutterStandardTypedData + let index = args["index"] as! Int32 + + let scanSecretData = scanSecret.data + let spendPubData = spendPub.data + result(MwebdAddress(scanSecretData, spendPubData, index)) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + private func startServer(result: @escaping FlutterResult) { + if CwMwebPlugin.server == nil { + var error: NSError? + CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, "", &error) + + if let server = CwMwebPlugin.server { + do { + print("Starting server...") + try server.start(0, ret0_: &CwMwebPlugin.port) + print("Server started successfully on port: \(CwMwebPlugin.port)") + result(CwMwebPlugin.port) + } catch let startError as NSError { + print("Server Start Error: \(startError.localizedDescription)") + result(FlutterError(code: "Server Start Error", message: startError.localizedDescription, details: nil)) + } + } else if let error = error { + print("Server Creation Error: \(error.localizedDescription)") + result(FlutterError(code: "Server Creation Error", message: error.localizedDescription, details: nil)) + } else { + print("Unknown Error: Failed to create server") + result(FlutterError(code: "Unknown Error", message: "Failed to create server", details: nil)) + } + } else { + print("Server already running on port: \(CwMwebPlugin.port)") + result(CwMwebPlugin.port) + } + } + + private func stopServer() { + print("Stopping server") + CwMwebPlugin.server?.stop() + CwMwebPlugin.server = nil + CwMwebPlugin.port = 0 + } + + deinit { + stopServer() + } +} diff --git a/cw_mweb/ios/cw_mweb.podspec b/cw_mweb/ios/cw_mweb.podspec new file mode 100644 index 000000000..4a1903bae --- /dev/null +++ b/cw_mweb/ios/cw_mweb.podspec @@ -0,0 +1,26 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_mweb.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_mweb' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + s.ios.vendored_frameworks = 'Mwebd.xcframework' + s.preserve_paths = 'Mwebd.xcframework/**/*' + +end diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart new file mode 100644 index 000000000..63ff1bf97 --- /dev/null +++ b/cw_mweb/lib/cw_mweb.dart @@ -0,0 +1,133 @@ +import 'dart:typed_data'; + +import 'package:grpc/grpc.dart'; +import 'package:path_provider/path_provider.dart'; +import 'cw_mweb_platform_interface.dart'; +import 'mwebd.pbgrpc.dart'; + +class CwMweb { + static RpcClient? _rpcClient; + static ClientChannel? _clientChannel; + static int? _port; + static const TIMEOUT_DURATION = Duration(seconds: 5); + + static Future _initializeClient() async { + await stop(); + // wait a few seconds to make sure the server is stopped + await Future.delayed(const Duration(seconds: 5)); + + final appDir = await getApplicationSupportDirectory(); + _port = await CwMwebPlatform.instance.start(appDir.path); + if (_port == null || _port == 0) { + throw Exception("Failed to start server"); + } + print("Attempting to connect to server on port: $_port"); + + // wait for the server to finish starting up before we try to connect to it: + await Future.delayed(const Duration(seconds: 5)); + + _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () { + print("Channel is shutting down!"); + }, + options: const ChannelOptions( + credentials: ChannelCredentials.insecure(), + keepAlive: ClientKeepAliveOptions(permitWithoutCalls: true), + )); + _rpcClient = RpcClient(_clientChannel!); + } + + static Future stub({int maxRetries = 3}) async { + for (int i = 0; i < maxRetries; i++) { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + final status = await _rpcClient! + .status(StatusRequest(), options: CallOptions(timeout: TIMEOUT_DURATION)); + if (status.blockTime == 0) { + throw Exception("blockTime shouldn't be 0! (this connection is likely broken)"); + } + return _rpcClient!; + } catch (e) { + print("Attempt $i failed: $e"); + _rpcClient = null; + } + } + throw Exception("Failed to connect after $maxRetries attempts"); + } + + static Future stop() async { + try { + await CwMwebPlatform.instance.stop(); + await cleanup(); + } catch (e) { + print("Error stopping server: $e"); + } + } + + static Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { + try { + return CwMwebPlatform.instance.address(scanSecret, spendPub, index); + } catch (e) { + print("Error getting address: $e"); + return null; + } + } + + static Future cleanup() async { + await _clientChannel?.terminate(); + _rpcClient = null; + _clientChannel = null; + _port = null; + } + + // wrappers that handle the connection issues: + static Future spent(SpentRequest request) async { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + return await _rpcClient!.spent(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } catch (e) { + print("Error getting spent: $e"); + return SpentResponse(); + } + } + + static Future status(StatusRequest request) async { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + return await _rpcClient!.status(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } catch (e) { + print("Error getting status: $e"); + return StatusResponse(); + } + } + + static Future create(CreateRequest request) async { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + return await _rpcClient!.create(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } catch (e) { + print("Error getting create: $e"); + return CreateResponse(); + } + } + + static Future?> utxos(UtxosRequest request) async { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + // this is a stream, so we should have an effectively infinite timeout: + return _rpcClient!.utxos(request, options: CallOptions(timeout: const Duration(days: 1000 * 365))); + } catch (e) { + print("Error getting utxos: $e"); + return null; + } + } +} diff --git a/cw_mweb/lib/cw_mweb_method_channel.dart b/cw_mweb/lib/cw_mweb_method_channel.dart new file mode 100644 index 000000000..70e4a1789 --- /dev/null +++ b/cw_mweb/lib/cw_mweb_method_channel.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'cw_mweb_platform_interface.dart'; + +/// An implementation of [CwMwebPlatform] that uses method channels. +class MethodChannelCwMweb extends CwMwebPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('cw_mweb'); + + @override + Future start(String dataDir) async { + final result = await methodChannel.invokeMethod('start', {'dataDir': dataDir}); + return result; + } + + @override + Future stop() async { + await methodChannel.invokeMethod('stop'); + } + + @override + Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { + final result = await methodChannel.invokeMethod('address', { + 'scanSecret': scanSecret, + 'spendPub': spendPub, + 'index': index, + }); + return result; + } +} diff --git a/cw_mweb/lib/cw_mweb_platform_interface.dart b/cw_mweb/lib/cw_mweb_platform_interface.dart new file mode 100644 index 000000000..8cc80f3e9 --- /dev/null +++ b/cw_mweb/lib/cw_mweb_platform_interface.dart @@ -0,0 +1,39 @@ +import 'dart:typed_data'; + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'cw_mweb_method_channel.dart'; + +abstract class CwMwebPlatform extends PlatformInterface { + /// Constructs a CwMwebPlatform. + CwMwebPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CwMwebPlatform _instance = MethodChannelCwMweb(); + + /// The default instance of [CwMwebPlatform] to use. + /// + /// Defaults to [MethodChannelCwMweb]. + static CwMwebPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CwMwebPlatform] when + /// they register themselves. + static set instance(CwMwebPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future start(String dataDir) { + throw UnimplementedError('start() has not been implemented.'); + } + + Future stop() { + throw UnimplementedError('stop() has not been implemented.'); + } + + Future address(Uint8List scanSecret, Uint8List spendPub, int index) { + throw UnimplementedError('address(int) has not been implemented.'); + } +} diff --git a/cw_mweb/lib/mwebd.pb.dart b/cw_mweb/lib/mwebd.pb.dart new file mode 100644 index 000000000..d0dd486c0 --- /dev/null +++ b/cw_mweb/lib/mwebd.pb.dart @@ -0,0 +1,801 @@ +// +// Generated code. Do not modify. +// source: mwebd.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +class StatusRequest extends $pb.GeneratedMessage { + factory StatusRequest() => create(); + StatusRequest._() : super(); + factory StatusRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory StatusRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusRequest', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StatusRequest clone() => StatusRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StatusRequest copyWith(void Function(StatusRequest) updates) => super.copyWith((message) => updates(message as StatusRequest)) as StatusRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StatusRequest create() => StatusRequest._(); + StatusRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StatusRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static StatusRequest? _defaultInstance; +} + +class StatusResponse extends $pb.GeneratedMessage { + factory StatusResponse({ + $core.int? blockHeaderHeight, + $core.int? mwebHeaderHeight, + $core.int? mwebUtxosHeight, + $core.int? blockTime, + }) { + final $result = create(); + if (blockHeaderHeight != null) { + $result.blockHeaderHeight = blockHeaderHeight; + } + if (mwebHeaderHeight != null) { + $result.mwebHeaderHeight = mwebHeaderHeight; + } + if (mwebUtxosHeight != null) { + $result.mwebUtxosHeight = mwebUtxosHeight; + } + if (blockTime != null) { + $result.blockTime = blockTime; + } + return $result; + } + StatusResponse._() : super(); + factory StatusResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory StatusResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusResponse', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'blockHeaderHeight', $pb.PbFieldType.O3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'mwebHeaderHeight', $pb.PbFieldType.O3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'mwebUtxosHeight', $pb.PbFieldType.O3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StatusResponse clone() => StatusResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StatusResponse copyWith(void Function(StatusResponse) updates) => super.copyWith((message) => updates(message as StatusResponse)) as StatusResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StatusResponse create() => StatusResponse._(); + StatusResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StatusResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static StatusResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get blockHeaderHeight => $_getIZ(0); + @$pb.TagNumber(1) + set blockHeaderHeight($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasBlockHeaderHeight() => $_has(0); + @$pb.TagNumber(1) + void clearBlockHeaderHeight() => clearField(1); + + @$pb.TagNumber(2) + $core.int get mwebHeaderHeight => $_getIZ(1); + @$pb.TagNumber(2) + set mwebHeaderHeight($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasMwebHeaderHeight() => $_has(1); + @$pb.TagNumber(2) + void clearMwebHeaderHeight() => clearField(2); + + @$pb.TagNumber(3) + $core.int get mwebUtxosHeight => $_getIZ(2); + @$pb.TagNumber(3) + set mwebUtxosHeight($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasMwebUtxosHeight() => $_has(2); + @$pb.TagNumber(3) + void clearMwebUtxosHeight() => clearField(3); + + @$pb.TagNumber(4) + $core.int get blockTime => $_getIZ(3); + @$pb.TagNumber(4) + set blockTime($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasBlockTime() => $_has(3); + @$pb.TagNumber(4) + void clearBlockTime() => clearField(4); +} + +class UtxosRequest extends $pb.GeneratedMessage { + factory UtxosRequest({ + $core.int? fromHeight, + $core.List<$core.int>? scanSecret, + }) { + final $result = create(); + if (fromHeight != null) { + $result.fromHeight = fromHeight; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + return $result; + } + UtxosRequest._() : super(); + factory UtxosRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory UtxosRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UtxosRequest', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromHeight', $pb.PbFieldType.O3) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + UtxosRequest clone() => UtxosRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + UtxosRequest copyWith(void Function(UtxosRequest) updates) => super.copyWith((message) => updates(message as UtxosRequest)) as UtxosRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UtxosRequest create() => UtxosRequest._(); + UtxosRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static UtxosRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static UtxosRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get fromHeight => $_getIZ(0); + @$pb.TagNumber(1) + set fromHeight($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFromHeight() => $_has(0); + @$pb.TagNumber(1) + void clearFromHeight() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get scanSecret => $_getN(1); + @$pb.TagNumber(2) + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasScanSecret() => $_has(1); + @$pb.TagNumber(2) + void clearScanSecret() => clearField(2); +} + +class Utxo extends $pb.GeneratedMessage { + factory Utxo({ + $core.int? height, + $fixnum.Int64? value, + $core.String? address, + $core.String? outputId, + $core.int? blockTime, + }) { + final $result = create(); + if (height != null) { + $result.height = height; + } + if (value != null) { + $result.value = value; + } + if (address != null) { + $result.address = address; + } + if (outputId != null) { + $result.outputId = outputId; + } + if (blockTime != null) { + $result.blockTime = blockTime; + } + return $result; + } + Utxo._() : super(); + factory Utxo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Utxo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Utxo', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'height', $pb.PbFieldType.O3) + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOS(3, _omitFieldNames ? '' : 'address') + ..aOS(4, _omitFieldNames ? '' : 'outputId') + ..a<$core.int>(5, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Utxo clone() => Utxo()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Utxo copyWith(void Function(Utxo) updates) => super.copyWith((message) => updates(message as Utxo)) as Utxo; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Utxo create() => Utxo._(); + Utxo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Utxo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Utxo? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get height => $_getIZ(0); + @$pb.TagNumber(1) + set height($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasHeight() => $_has(0); + @$pb.TagNumber(1) + void clearHeight() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get value => $_getI64(1); + @$pb.TagNumber(2) + set value($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasValue() => $_has(1); + @$pb.TagNumber(2) + void clearValue() => clearField(2); + + @$pb.TagNumber(3) + $core.String get address => $_getSZ(2); + @$pb.TagNumber(3) + set address($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasAddress() => $_has(2); + @$pb.TagNumber(3) + void clearAddress() => clearField(3); + + @$pb.TagNumber(4) + $core.String get outputId => $_getSZ(3); + @$pb.TagNumber(4) + set outputId($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasOutputId() => $_has(3); + @$pb.TagNumber(4) + void clearOutputId() => clearField(4); + + @$pb.TagNumber(5) + $core.int get blockTime => $_getIZ(4); + @$pb.TagNumber(5) + set blockTime($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasBlockTime() => $_has(4); + @$pb.TagNumber(5) + void clearBlockTime() => clearField(5); +} + +class AddressRequest extends $pb.GeneratedMessage { + factory AddressRequest({ + $core.int? fromIndex, + $core.int? toIndex, + $core.List<$core.int>? scanSecret, + $core.List<$core.int>? spendPubkey, + }) { + final $result = create(); + if (fromIndex != null) { + $result.fromIndex = fromIndex; + } + if (toIndex != null) { + $result.toIndex = toIndex; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + if (spendPubkey != null) { + $result.spendPubkey = spendPubkey; + } + return $result; + } + AddressRequest._() : super(); + factory AddressRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AddressRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressRequest', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromIndex', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'toIndex', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'spendPubkey', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AddressRequest clone() => AddressRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AddressRequest copyWith(void Function(AddressRequest) updates) => super.copyWith((message) => updates(message as AddressRequest)) as AddressRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AddressRequest create() => AddressRequest._(); + AddressRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AddressRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AddressRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get fromIndex => $_getIZ(0); + @$pb.TagNumber(1) + set fromIndex($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFromIndex() => $_has(0); + @$pb.TagNumber(1) + void clearFromIndex() => clearField(1); + + @$pb.TagNumber(2) + $core.int get toIndex => $_getIZ(1); + @$pb.TagNumber(2) + set toIndex($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasToIndex() => $_has(1); + @$pb.TagNumber(2) + void clearToIndex() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get scanSecret => $_getN(2); + @$pb.TagNumber(3) + set scanSecret($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasScanSecret() => $_has(2); + @$pb.TagNumber(3) + void clearScanSecret() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get spendPubkey => $_getN(3); + @$pb.TagNumber(4) + set spendPubkey($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasSpendPubkey() => $_has(3); + @$pb.TagNumber(4) + void clearSpendPubkey() => clearField(4); +} + +class AddressResponse extends $pb.GeneratedMessage { + factory AddressResponse({ + $core.Iterable<$core.String>? address, + }) { + final $result = create(); + if (address != null) { + $result.address.addAll(address); + } + return $result; + } + AddressResponse._() : super(); + factory AddressResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AddressResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressResponse', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'address') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AddressResponse clone() => AddressResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AddressResponse copyWith(void Function(AddressResponse) updates) => super.copyWith((message) => updates(message as AddressResponse)) as AddressResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AddressResponse create() => AddressResponse._(); + AddressResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AddressResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AddressResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get address => $_getList(0); +} + +class SpentRequest extends $pb.GeneratedMessage { + factory SpentRequest({ + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + SpentRequest._() : super(); + factory SpentRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpentRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentRequest', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpentRequest clone() => SpentRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpentRequest copyWith(void Function(SpentRequest) updates) => super.copyWith((message) => updates(message as SpentRequest)) as SpentRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpentRequest create() => SpentRequest._(); + SpentRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpentRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpentRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get outputId => $_getList(0); +} + +class SpentResponse extends $pb.GeneratedMessage { + factory SpentResponse({ + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + SpentResponse._() : super(); + factory SpentResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpentResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentResponse', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpentResponse clone() => SpentResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpentResponse copyWith(void Function(SpentResponse) updates) => super.copyWith((message) => updates(message as SpentResponse)) as SpentResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpentResponse create() => SpentResponse._(); + SpentResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpentResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpentResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get outputId => $_getList(0); +} + +class CreateRequest extends $pb.GeneratedMessage { + factory CreateRequest({ + $core.List<$core.int>? rawTx, + $core.List<$core.int>? scanSecret, + $core.List<$core.int>? spendSecret, + $fixnum.Int64? feeRatePerKb, + $core.bool? dryRun, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + if (spendSecret != null) { + $result.spendSecret = spendSecret; + } + if (feeRatePerKb != null) { + $result.feeRatePerKb = feeRatePerKb; + } + if (dryRun != null) { + $result.dryRun = dryRun; + } + return $result; + } + CreateRequest._() : super(); + factory CreateRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateRequest', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'spendSecret', $pb.PbFieldType.OY) + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'feeRatePerKb', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOB(5, _omitFieldNames ? '' : 'dryRun') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateRequest clone() => CreateRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateRequest copyWith(void Function(CreateRequest) updates) => super.copyWith((message) => updates(message as CreateRequest)) as CreateRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateRequest create() => CreateRequest._(); + CreateRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CreateRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CreateRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get scanSecret => $_getN(1); + @$pb.TagNumber(2) + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasScanSecret() => $_has(1); + @$pb.TagNumber(2) + void clearScanSecret() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get spendSecret => $_getN(2); + @$pb.TagNumber(3) + set spendSecret($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasSpendSecret() => $_has(2); + @$pb.TagNumber(3) + void clearSpendSecret() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get feeRatePerKb => $_getI64(3); + @$pb.TagNumber(4) + set feeRatePerKb($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasFeeRatePerKb() => $_has(3); + @$pb.TagNumber(4) + void clearFeeRatePerKb() => clearField(4); + + @$pb.TagNumber(5) + $core.bool get dryRun => $_getBF(4); + @$pb.TagNumber(5) + set dryRun($core.bool v) { $_setBool(4, v); } + @$pb.TagNumber(5) + $core.bool hasDryRun() => $_has(4); + @$pb.TagNumber(5) + void clearDryRun() => clearField(5); +} + +class CreateResponse extends $pb.GeneratedMessage { + factory CreateResponse({ + $core.List<$core.int>? rawTx, + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + CreateResponse._() : super(); + factory CreateResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateResponse', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..pPS(2, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateResponse clone() => CreateResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateResponse copyWith(void Function(CreateResponse) updates) => super.copyWith((message) => updates(message as CreateResponse)) as CreateResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateResponse create() => CreateResponse._(); + CreateResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CreateResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CreateResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.String> get outputId => $_getList(1); +} + +class BroadcastRequest extends $pb.GeneratedMessage { + factory BroadcastRequest({ + $core.List<$core.int>? rawTx, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + return $result; + } + BroadcastRequest._() : super(); + factory BroadcastRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BroadcastRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastRequest', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BroadcastRequest clone() => BroadcastRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BroadcastRequest copyWith(void Function(BroadcastRequest) updates) => super.copyWith((message) => updates(message as BroadcastRequest)) as BroadcastRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BroadcastRequest create() => BroadcastRequest._(); + BroadcastRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BroadcastRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BroadcastRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); +} + +class BroadcastResponse extends $pb.GeneratedMessage { + factory BroadcastResponse({ + $core.String? txid, + }) { + final $result = create(); + if (txid != null) { + $result.txid = txid; + } + return $result; + } + BroadcastResponse._() : super(); + factory BroadcastResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BroadcastResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastResponse', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'txid') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BroadcastResponse clone() => BroadcastResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BroadcastResponse copyWith(void Function(BroadcastResponse) updates) => super.copyWith((message) => updates(message as BroadcastResponse)) as BroadcastResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BroadcastResponse create() => BroadcastResponse._(); + BroadcastResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BroadcastResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BroadcastResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get txid => $_getSZ(0); + @$pb.TagNumber(1) + set txid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasTxid() => $_has(0); + @$pb.TagNumber(1) + void clearTxid() => clearField(1); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/cw_mweb/lib/mwebd.pbgrpc.dart b/cw_mweb/lib/mwebd.pbgrpc.dart new file mode 100644 index 000000000..6bc48cfdf --- /dev/null +++ b/cw_mweb/lib/mwebd.pbgrpc.dart @@ -0,0 +1,159 @@ +// +// Generated code. Do not modify. +// source: mwebd.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:async' as $async; +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'mwebd.pb.dart' as $0; + +export 'mwebd.pb.dart'; + +@$pb.GrpcServiceName('Rpc') +class RpcClient extends $grpc.Client { + static final _$status = $grpc.ClientMethod<$0.StatusRequest, $0.StatusResponse>( + '/Rpc/Status', + ($0.StatusRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.StatusResponse.fromBuffer(value)); + static final _$utxos = $grpc.ClientMethod<$0.UtxosRequest, $0.Utxo>( + '/Rpc/Utxos', + ($0.UtxosRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Utxo.fromBuffer(value)); + static final _$addresses = $grpc.ClientMethod<$0.AddressRequest, $0.AddressResponse>( + '/Rpc/Addresses', + ($0.AddressRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.AddressResponse.fromBuffer(value)); + static final _$spent = $grpc.ClientMethod<$0.SpentRequest, $0.SpentResponse>( + '/Rpc/Spent', + ($0.SpentRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.SpentResponse.fromBuffer(value)); + static final _$create = $grpc.ClientMethod<$0.CreateRequest, $0.CreateResponse>( + '/Rpc/Create', + ($0.CreateRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.CreateResponse.fromBuffer(value)); + static final _$broadcast = $grpc.ClientMethod<$0.BroadcastRequest, $0.BroadcastResponse>( + '/Rpc/Broadcast', + ($0.BroadcastRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.BroadcastResponse.fromBuffer(value)); + + RpcClient($grpc.ClientChannel channel, + {$grpc.CallOptions? options, + $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + : super(channel, options: options, + interceptors: interceptors); + + $grpc.ResponseFuture<$0.StatusResponse> status($0.StatusRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$status, request, options: options); + } + + $grpc.ResponseStream<$0.Utxo> utxos($0.UtxosRequest request, {$grpc.CallOptions? options}) { + return $createStreamingCall(_$utxos, $async.Stream.fromIterable([request]), options: options); + } + + $grpc.ResponseFuture<$0.AddressResponse> addresses($0.AddressRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$addresses, request, options: options); + } + + $grpc.ResponseFuture<$0.SpentResponse> spent($0.SpentRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$spent, request, options: options); + } + + $grpc.ResponseFuture<$0.CreateResponse> create($0.CreateRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$create, request, options: options); + } + + $grpc.ResponseFuture<$0.BroadcastResponse> broadcast($0.BroadcastRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$broadcast, request, options: options); + } +} + +@$pb.GrpcServiceName('Rpc') +abstract class RpcServiceBase extends $grpc.Service { + $core.String get $name => 'Rpc'; + + RpcServiceBase() { + $addMethod($grpc.ServiceMethod<$0.StatusRequest, $0.StatusResponse>( + 'Status', + status_Pre, + false, + false, + ($core.List<$core.int> value) => $0.StatusRequest.fromBuffer(value), + ($0.StatusResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.UtxosRequest, $0.Utxo>( + 'Utxos', + utxos_Pre, + false, + true, + ($core.List<$core.int> value) => $0.UtxosRequest.fromBuffer(value), + ($0.Utxo value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.AddressRequest, $0.AddressResponse>( + 'Addresses', + addresses_Pre, + false, + false, + ($core.List<$core.int> value) => $0.AddressRequest.fromBuffer(value), + ($0.AddressResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.SpentRequest, $0.SpentResponse>( + 'Spent', + spent_Pre, + false, + false, + ($core.List<$core.int> value) => $0.SpentRequest.fromBuffer(value), + ($0.SpentResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.CreateRequest, $0.CreateResponse>( + 'Create', + create_Pre, + false, + false, + ($core.List<$core.int> value) => $0.CreateRequest.fromBuffer(value), + ($0.CreateResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.BroadcastRequest, $0.BroadcastResponse>( + 'Broadcast', + broadcast_Pre, + false, + false, + ($core.List<$core.int> value) => $0.BroadcastRequest.fromBuffer(value), + ($0.BroadcastResponse value) => value.writeToBuffer())); + } + + $async.Future<$0.StatusResponse> status_Pre($grpc.ServiceCall call, $async.Future<$0.StatusRequest> request) async { + return status(call, await request); + } + + $async.Stream<$0.Utxo> utxos_Pre($grpc.ServiceCall call, $async.Future<$0.UtxosRequest> request) async* { + yield* utxos(call, await request); + } + + $async.Future<$0.AddressResponse> addresses_Pre($grpc.ServiceCall call, $async.Future<$0.AddressRequest> request) async { + return addresses(call, await request); + } + + $async.Future<$0.SpentResponse> spent_Pre($grpc.ServiceCall call, $async.Future<$0.SpentRequest> request) async { + return spent(call, await request); + } + + $async.Future<$0.CreateResponse> create_Pre($grpc.ServiceCall call, $async.Future<$0.CreateRequest> request) async { + return create(call, await request); + } + + $async.Future<$0.BroadcastResponse> broadcast_Pre($grpc.ServiceCall call, $async.Future<$0.BroadcastRequest> request) async { + return broadcast(call, await request); + } + + $async.Future<$0.StatusResponse> status($grpc.ServiceCall call, $0.StatusRequest request); + $async.Stream<$0.Utxo> utxos($grpc.ServiceCall call, $0.UtxosRequest request); + $async.Future<$0.AddressResponse> addresses($grpc.ServiceCall call, $0.AddressRequest request); + $async.Future<$0.SpentResponse> spent($grpc.ServiceCall call, $0.SpentRequest request); + $async.Future<$0.CreateResponse> create($grpc.ServiceCall call, $0.CreateRequest request); + $async.Future<$0.BroadcastResponse> broadcast($grpc.ServiceCall call, $0.BroadcastRequest request); +} diff --git a/cw_mweb/macos/Classes/CwMwebPlugin.swift b/cw_mweb/macos/Classes/CwMwebPlugin.swift new file mode 100644 index 000000000..9c0dabd40 --- /dev/null +++ b/cw_mweb/macos/Classes/CwMwebPlugin.swift @@ -0,0 +1,19 @@ +import Cocoa +import FlutterMacOS + +public class CwMwebPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger) + let instance = CwMwebPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_mweb/macos/cw_mweb.podspec b/cw_mweb/macos/cw_mweb.podspec new file mode 100644 index 000000000..8fadcced9 --- /dev/null +++ b/cw_mweb/macos/cw_mweb.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_mweb.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_mweb' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/cw_mweb/pubspec.yaml b/cw_mweb/pubspec.yaml new file mode 100644 index 000000000..cfe43c70b --- /dev/null +++ b/cw_mweb/pubspec.yaml @@ -0,0 +1,76 @@ +name: cw_mweb +description: A new Flutter plugin project. +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + grpc: ^3.2.4 + path_provider: ^2.1.2 + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.cakewallet.mweb + pluginClass: CwMwebPlugin + ios: + pluginClass: CwMwebPlugin + macos: + pluginClass: CwMwebPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/ios/.gitignore b/ios/.gitignore index e96ef602b..8ded86f14 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -30,3 +30,5 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 + +Mwebd.xcframework \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 847769cba..e574aafc1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,6 +7,7 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.2) + - cw_mweb (0.0.1): - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -111,6 +112,7 @@ DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift + - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -156,6 +158,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/barcode_scan2/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cw_mweb: + :path: ".symlinks/plugins/cw_mweb/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -211,6 +215,7 @@ SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea + cw_mweb: 87af74f9659fed0c1a2cbfb44413f1070e79e3ae device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: b22617f40038496deffba44747101255cee005b0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 688fa2c39..10cc6434d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; CE291CFE2C15DB9A00B9F709 /* WowneroWallet.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C58D93382C00FAC6004BCF69 /* libresolv.tbd */; }; CFEFC24F82F78FE747DF1D22 /* LnurlPayInfo.swift in Resources */ = {isa = PBXBuildFile; fileRef = 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; D0D7A0D4E13F31C4E02E235B /* ReceivePayment.swift in Resources */ = {isa = PBXBuildFile; fileRef = 91C524F800843E0A3F17E004 /* ReceivePayment.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; D3AD73A327249AFE8F016A51 /* BreezSDK.swift in Resources */ = {isa = PBXBuildFile; fileRef = ABD6FCBB0F4244B090459128 /* BreezSDK.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; @@ -84,7 +85,9 @@ ABD6FCBB0F4244B090459128 /* BreezSDK.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDK.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDK.swift"; sourceTree = ""; }; AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = ""; }; + C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WowneroWallet.framework; sourceTree = ""; }; + CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Mwebd.xcframework; sourceTree = ""; }; DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInvoice.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInvoice.swift"; sourceTree = ""; }; F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceLogger.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceLogger.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -95,6 +98,7 @@ buildActionMask = 2147483647; files = ( 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */, + CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -104,6 +108,8 @@ 06957875428D0F5AAE053765 /* Frameworks */ = { isa = PBXGroup; children = ( + CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */, + C58D93382C00FAC6004BCF69 /* libresolv.tbd */, 0C9986A3251A932F00D566FD /* CryptoSwift.framework */, 3C663361C56EBB242598F609 /* Pods_Runner.framework */, ); diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index d1869a05d..50c22d113 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -220,9 +220,9 @@ class CWBitcoin extends Bitcoin { return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); } - WalletService createLitecoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool isDirect) { - return LitecoinWalletService(walletInfoSource, unspentCoinSource, isDirect); + WalletService createLitecoinWalletService(Box walletInfoSource, + Box unspentCoinSource, bool alwaysScan, bool isDirect) { + return LitecoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); } @override @@ -261,6 +261,9 @@ class CWBitcoin extends Bitcoin { @override List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; + @override + List getLitecoinReceivePageOptions() => BitcoinReceivePageOption.allLitecoin; + @override BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { switch (option) { @@ -272,6 +275,8 @@ class CWBitcoin extends Bitcoin { return SegwitAddresType.p2tr; case BitcoinReceivePageOption.p2wsh: return SegwitAddresType.p2wsh; + case BitcoinReceivePageOption.mweb: + return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: default: return SegwitAddresType.p2wpkh; @@ -555,6 +560,9 @@ class CWBitcoin extends Bitcoin { return await getBitcoinHeightByDate(date: date); } + @override + int getLitecoinHeightByDate({required DateTime date}) => getLtcHeightByDate(date: date); + @override Future rescan(Object wallet, {required int height, bool? doSingleScan}) async { final bitcoinWallet = wallet as ElectrumWallet; @@ -580,6 +588,17 @@ class CWBitcoin extends Bitcoin { } @override + Future setMwebEnabled(Object wallet, bool enabled) async { + final litecoinWallet = wallet as LitecoinWallet; + litecoinWallet.setMwebEnabled(enabled); + } + + @override + bool getMwebEnabled(Object wallet) { + final litecoinWallet = wallet as LitecoinWallet; + return litecoinWallet.mwebEnabled; + } + List updateOutputs(PendingTransaction pendingTransaction, List outputs) { final pendingTx = pendingTransaction as PendingBitcoinTransaction; @@ -588,7 +607,6 @@ class CWBitcoin extends Bitcoin { } final updatedOutputs = outputs.map((output) { - try { final pendingOut = pendingTx!.outputs[outputs.indexOf(output)]; final updatedOutput = output; @@ -609,4 +627,31 @@ class CWBitcoin extends Bitcoin { final tx = txInfo as ElectrumTransactionInfo; return tx.isReceivedSilentPayment; } + + @override + bool txIsMweb(TransactionInfo txInfo) { + final tx = txInfo as ElectrumTransactionInfo; + + List inputAddresses = tx.inputAddresses ?? []; + List outputAddresses = tx.outputAddresses ?? []; + bool inputAddressesContainMweb = false; + bool outputAddressesContainMweb = false; + + for (var address in inputAddresses) { + if (address.toLowerCase().contains('mweb')) { + inputAddressesContainMweb = true; + break; + } + } + + for (var address in outputAddresses) { + if (address.toLowerCase().contains('mweb')) { + outputAddressesContainMweb = true; + break; + } + } + + // TODO: this could be improved: + return inputAddressesContainMweb || outputAddressesContainMweb; + } } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 06a30a2c3..0aabfa032 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -12,10 +12,12 @@ class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) : super( errorMessage: S.current.error_text_address, - useAdditionalValidation: type == CryptoCurrency.btc + useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc ? (String txt) => BitcoinAddressUtils.validateAddress( address: txt, - network: BitcoinNetwork.mainnet, + network: type == CryptoCurrency.btc + ? BitcoinNetwork.mainnet + : LitecoinNetwork.mainnet, ) : null, pattern: getPattern(type), @@ -35,6 +37,8 @@ class AddressValidator extends TextValidator { case CryptoCurrency.btc: pattern = '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + case CryptoCurrency.ltc: + pattern = '^${P2wpkhAddress.regex.pattern}\$|^${MwebAddress.regex.pattern}\$'; case CryptoCurrency.nano: pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.banano: @@ -106,8 +110,6 @@ class AddressValidator extends TextValidator { '(?!bitcoincash:)[0-9a-zA-Z]*|(?!bitcoincash:)q|p[0-9a-zA-Z]{41}|(?!bitcoincash:)q|p[0-9a-zA-Z]{42}|bitcoincash:q|p[0-9a-zA-Z]{41}|bitcoincash:q|p[0-9a-zA-Z]{42}'; case CryptoCurrency.bnb: pattern = '[0-9a-zA-Z]+'; - case CryptoCurrency.ltc: - pattern = '((?!(ltc|LTC)1)[0-9a-zA-Z]*)|(LTC1[A-Z0-9]*)|(ltc1[a-z0-9]*)'; case CryptoCurrency.hbar: pattern = '[0-9a-zA-Z.]+'; case CryptoCurrency.zaddr: @@ -157,6 +159,8 @@ class AddressValidator extends TextValidator { return null; case CryptoCurrency.btc: return null; + case CryptoCurrency.ltc: + return null; case CryptoCurrency.dash: return [34]; case CryptoCurrency.eos: @@ -203,8 +207,6 @@ class AddressValidator extends TextValidator { return [42, 43, 44, 54, 55]; case CryptoCurrency.bnb: return [42]; - case CryptoCurrency.ltc: - return [34, 43, 63]; case CryptoCurrency.nano: return [64, 65]; case CryptoCurrency.banano: @@ -289,9 +291,10 @@ class AddressValidator extends TextValidator { pattern = '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: - pattern = '(L[a-zA-Z0-9]{26,33})' - '|([LM][a-km-zA-HJ-NP-Z1-9]{26,33})' - '|(ltc[a-zA-Z0-9]{26,45})'; + pattern = '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)((ltc|t)mweb1q[ac-hj-np-z02-9]{90,120})([^0-9a-zA-Z]|\$)'; case CryptoCurrency.eth: case CryptoCurrency.maticpoly: pattern = '0x[0-9a-zA-Z]+'; diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 0087b1332..e58e14652 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -85,7 +85,8 @@ class WalletLoadingService { authenticatedErrorStreamController.add(corruptedWalletsSeeds); return wallet; - } catch (_) { + } catch (e) { + print(e); // save seeds and show corrupted wallets' seeds to the user try { final seeds = await _getCorruptedWalletSeeds(walletInfo.name, walletInfo.type); diff --git a/lib/di.dart b/lib/di.dart index 0b98244e6..4a1952729 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -106,6 +106,7 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_settings.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; @@ -156,6 +157,7 @@ import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; @@ -671,7 +673,8 @@ Future setup({ getIt.registerFactory( () => Modify2FAPage(setup2FAViewModel: getIt.get())); - getIt.registerFactory(() => DesktopSettingsPage(getIt.get())); + getIt.registerFactory( + () => DesktopSettingsPage(getIt.get())); getIt.registerFactoryParam( (pageOption, _) => ReceiveOptionViewModel(getIt.get().wallet!, pageOption)); @@ -803,7 +806,9 @@ Future setup({ getIt.registerFactory(() { final wallet = getIt.get().wallet!; - if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) { + if (wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || + wallet.type == WalletType.haven) { return MoneroAccountListViewModel(wallet); } throw Exception( @@ -863,6 +868,9 @@ Future setup({ getIt.registerFactory(() => SilentPaymentsSettingsViewModel(getIt.get(), getIt.get().wallet!)); + getIt.registerFactory( + () => MwebSettingsViewModel(getIt.get(), getIt.get().wallet!)); + getIt.registerFactory(() { return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!); }); @@ -929,6 +937,8 @@ Future setup({ getIt.registerFactory( () => SilentPaymentsSettingsPage(getIt.get())); + getIt.registerFactory(() => MwebSettingsPage(getIt.get())); + getIt.registerFactory(() => OtherSettingsPage(getIt.get())); getIt.registerFactory(() => NanoChangeRepPage( @@ -1023,8 +1033,12 @@ Future setup({ SettingsStoreBase.walletPasswordDirectInput, ); case WalletType.litecoin: - return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource, - SettingsStoreBase.walletPasswordDirectInput); + return bitcoin!.createLitecoinWalletService( + _walletInfoSource, + _unspentCoinsInfoSource, + getIt.get().mwebAlwaysScan, + SettingsStoreBase.walletPasswordDirectInput, + ); case WalletType.ethereum: return ethereum!.createEthereumWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); @@ -1236,7 +1250,8 @@ Future setup({ getIt.registerFactory( () => CakePayService(getIt.get(), getIt.get())); - getIt.registerFactory(() => CakePayCardsListViewModel(cakePayService: getIt.get())); + getIt.registerFactory( + () => CakePayCardsListViewModel(cakePayService: getIt.get())); getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get())); @@ -1268,12 +1283,12 @@ Future setup({ getIt.registerFactoryParam, void>((List args, _) { final vendor = args.first as CakePayVendor; - return CakePayBuyCardPage(getIt.get(param1: vendor), - getIt.get()); + return CakePayBuyCardPage( + getIt.get(param1: vendor), getIt.get()); }); - getIt.registerFactoryParam, void>( - (List args, _) { + getIt + .registerFactoryParam, void>((List args, _) { final paymentCredential = args.first as PaymentCredential; final card = args[1] as CakePayCard; return CakePayBuyCardDetailPage( diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 0c032a736..4fbe358e5 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -48,6 +48,10 @@ class PreferencesKey { static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; + static const mwebCardDisplay = 'mwebCardDisplay'; + static const mwebEnabled = 'mwebEnabled'; + static const hasEnabledMwebBefore = 'hasEnabledMwebBefore'; + static const mwebAlwaysScan = 'mwebAlwaysScan'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowRepWarning = 'should_show_rep_warning'; diff --git a/lib/main.dart b/lib/main.dart index 32a6397c2..dcfd8d0da 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,6 +30,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/address_info.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -152,6 +153,10 @@ Future initializeAppConfigs() async { CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter()); } + if (!CakeHive.isAdapterRegistered(MwebUtxo.typeId)) { + CakeHive.registerAdapter(MwebUtxoAdapter()); + } + final secureStorage = secureStorageShared; final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey); @@ -233,7 +238,6 @@ Future initialSetup( secureStorage: secureStorage, ); await bootstrap(navigatorKey); - monero?.onStartup(); } class App extends StatefulWidget { diff --git a/lib/router.dart b/lib/router.dart index 16eeefeb1..7beace174 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -72,6 +72,7 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_settings.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; @@ -450,6 +451,10 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.mwebSettings: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.connectionSync: return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); diff --git a/lib/routes.dart b/lib/routes.dart index 83d90248f..0529d7c6f 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -73,6 +73,7 @@ class Routes { static const cakePayAccountPage = '/cake_pay_account_page'; static const webViewPage = '/web_view_page'; static const silentPaymentsSettings = '/silent_payments_settings'; + static const mwebSettings = '/mweb_settings'; static const connectionSync = '/connection_sync_page'; static const securityBackupPage = '/security_and_backup_page'; static const privacyPage = '/privacy_page'; diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 3ac97740d..2040cbf9d 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -225,7 +225,8 @@ class AddressPage extends BasePage { } break; default: - if (addressListViewModel.type == WalletType.bitcoin) { + if (addressListViewModel.type == WalletType.bitcoin || + addressListViewModel.type == WalletType.litecoin) { addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); } } diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 57f908986..a7b2a99fa 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; @@ -25,6 +26,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; import 'package:url_launcher/url_launcher.dart'; class BalancePage extends StatelessWidget { @@ -129,7 +131,7 @@ class CryptoBalanceWidget extends StatelessWidget { builder: (_) { if (dashboardViewModel.getMoneroError != null) { return Padding( - padding: const EdgeInsets.fromLTRB(16,0,16,16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: DashBoardRoundedCardWidget( title: "Invalid monero bindings", subTitle: dashboardViewModel.getMoneroError.toString(), @@ -144,13 +146,12 @@ class CryptoBalanceWidget extends StatelessWidget { builder: (_) { if (dashboardViewModel.getWowneroError != null) { return Padding( - padding: const EdgeInsets.fromLTRB(16,0,16,16), - child: DashBoardRoundedCardWidget( - title: "Invalid wownero bindings", - subTitle: dashboardViewModel.getWowneroError.toString(), - onTap: () {}, - ) - ); + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: DashBoardRoundedCardWidget( + title: "Invalid wownero bindings", + subTitle: dashboardViewModel.getWowneroError.toString(), + onTap: () {}, + )); } return Container(); }, @@ -271,6 +272,18 @@ class CryptoBalanceWidget extends StatelessWidget { currency: balance.asset, hasAdditionalBalance: dashboardViewModel.balanceViewModel.hasAdditionalBalance, + hasSecondAdditionalBalance: + dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance, + hasSecondAvailableBalance: + dashboardViewModel.balanceViewModel.hasSecondAvailableBalance, + secondAdditionalBalance: balance.secondAdditionalBalance, + secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance, + secondAvailableBalance: balance.secondAvailableBalance, + secondAvailableFiatBalance: balance.fiatSecondAvailableBalance, + secondAdditionalBalanceLabel: + '${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}', + secondAvailableBalanceLabel: + '${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}', isTestnet: dashboardViewModel.isTestnet, ); }); @@ -284,16 +297,15 @@ class CryptoBalanceWidget extends StatelessWidget { if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[ SizedBox(height: 10), Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - customBorder: 30, - title: "This wallet has encountered an issue", - subTitle: "Here are the things that you should note:\n - " - +dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") - +"\n\nPlease restart your wallet and if it doesn't help contact our support.", - onTap: () {}, - ) - ) + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: "This wallet has encountered an issue", + subTitle: "Here are the things that you should note:\n - " + + dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") + + "\n\nPlease restart your wallet and if it doesn't help contact our support.", + onTap: () {}, + )) ], if (dashboardViewModel.showSilentPaymentsCard) ...[ SizedBox(height: 10), @@ -360,7 +372,73 @@ class CryptoBalanceWidget extends StatelessWidget { ), ), ), - ] + ], + if (dashboardViewModel.showMwebCard) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: S.current.litecoin_mweb, + subTitle: S.current.litecoin_enable_mweb_sync, + hint: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, + ), + child: Row( + children: [ + Text( + S.current.litecoin_what_is_mweb, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + softWrap: true, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ) + ], + ), + ), + Observer( + builder: (_) => StandardSwitch( + value: dashboardViewModel.mwebScanningActive, + onTaped: () => _toggleMweb(context), + ), + ) + ], + ), + ], + ), + onTap: () => _toggleMweb(context), + icon: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: + Theme.of(context).extension()!.pageTitleTextColor, + size: 50, + ), + ), + ), + ], ], ); }), @@ -400,6 +478,22 @@ class CryptoBalanceWidget extends StatelessWidget { return dashboardViewModel.setSilentPaymentsScanning(newValue); } + + Future _toggleMweb(BuildContext context) async { + if (!dashboardViewModel.hasEnabledMwebBefore) { + await showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).warning, + alertContent: S.current.litecoin_mweb_warning, + buttonText: S.of(context).ok, + buttonAction: () { + Navigator.of(context).pop(); + }, + )); + } + dashboardViewModel.setMwebScanningActive(!dashboardViewModel.mwebScanningActive); + } } class BalanceRowWidget extends StatelessWidget { @@ -410,10 +504,18 @@ class BalanceRowWidget extends StatelessWidget { required this.additionalBalanceLabel, required this.additionalBalance, required this.additionalFiatBalance, + required this.secondAvailableBalanceLabel, + required this.secondAvailableBalance, + required this.secondAvailableFiatBalance, + required this.secondAdditionalBalanceLabel, + required this.secondAdditionalBalance, + required this.secondAdditionalFiatBalance, required this.frozenBalance, required this.frozenFiatBalance, required this.currency, required this.hasAdditionalBalance, + required this.hasSecondAvailableBalance, + required this.hasSecondAdditionalBalance, required this.isTestnet, super.key, }); @@ -424,10 +526,18 @@ class BalanceRowWidget extends StatelessWidget { final String additionalBalanceLabel; final String additionalBalance; final String additionalFiatBalance; + final String secondAvailableBalanceLabel; + final String secondAvailableBalance; + final String secondAvailableFiatBalance; + final String secondAdditionalBalanceLabel; + final String secondAdditionalBalance; + final String secondAdditionalFiatBalance; final String frozenBalance; final String frozenFiatBalance; final CryptoCurrency currency; final bool hasAdditionalBalance; + final bool hasSecondAvailableBalance; + final bool hasSecondAdditionalBalance; final bool isTestnet; // void _showBalanceDescription(BuildContext context) { @@ -675,6 +785,94 @@ class BalanceRowWidget extends StatelessWidget { ), ], ), + if (hasSecondAvailableBalance) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAvailableBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAvailableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), + if (hasSecondAdditionalBalance) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAdditionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAdditionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAdditionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), ], ), ), diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 2538824ed..b6d1c286b 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -86,6 +86,18 @@ class TransactionsPage extends StatelessWidget { final transaction = item.transaction; final transactionType = dashboardViewModel.getTransactionType(transaction); + List tags = []; + if (dashboardViewModel.type == WalletType.bitcoin) { + if (bitcoin!.txIsReceivedSilentPayment(transaction)) { + tags.add(S.of(context).silent_payment); + } + } + if (dashboardViewModel.type == WalletType.litecoin) { + if (bitcoin!.txIsMweb(transaction)) { + tags.add("MWEB"); + } + } + return Observer( builder: (_) => TransactionRow( onTap: () => Navigator.of(context) @@ -100,9 +112,7 @@ class TransactionsPage extends StatelessWidget { isPending: transaction.isPending, title: item.formattedTitle + item.formattedStatus + transactionType, - isReceivedSilentPayment: - dashboardViewModel.type == WalletType.bitcoin && - bitcoin!.txIsReceivedSilentPayment(transaction), + tags: tags, ), ); } diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 1caeb4c51..30c7b7f78 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -191,6 +191,11 @@ class MenuWidgetState extends State { final item = items[index]; + if (!widget.dashboardViewModel.hasMweb && + item.name(context) == S.current.litecoin_mweb_settings) { + return const SizedBox(); + } + final isLastTile = index == itemCount - 1; return SettingActionButton( diff --git a/lib/src/screens/dashboard/widgets/transaction_raw.dart b/lib/src/screens/dashboard/widgets/transaction_raw.dart index 88866fa25..b18131f3d 100644 --- a/lib/src/screens/dashboard/widgets/transaction_raw.dart +++ b/lib/src/screens/dashboard/widgets/transaction_raw.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/transaction_direction.dart'; @@ -12,7 +11,7 @@ class TransactionRow extends StatelessWidget { required this.formattedAmount, required this.formattedFiatAmount, required this.isPending, - required this.isReceivedSilentPayment, + required this.tags, required this.title, required this.onTap, }); @@ -23,8 +22,8 @@ class TransactionRow extends StatelessWidget { final String formattedAmount; final String formattedFiatAmount; final bool isPending; - final bool isReceivedSilentPayment; final String title; + final List tags; @override Widget build(BuildContext context) { @@ -61,7 +60,7 @@ class TransactionRow extends StatelessWidget { fontWeight: FontWeight.w500, color: Theme.of(context).extension()!.textColor, )), - if (isReceivedSilentPayment) TxTag(tag: S.of(context).silent_payment), + ...tags.map((tag) => Row(children: [SizedBox(width: 8), TxTag(tag: tag)])), ], ), Text(formattedAmount, diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 8dfbedec1..27ec8c33a 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -1,3 +1,6 @@ + +import 'dart:math'; + import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index 58e7a44f6..2c1c213c1 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -33,7 +33,9 @@ class RescanPage extends BasePage { key: _blockchainHeightWidgetKey, onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value, isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan, + isMwebScan: _rescanViewModel.isMwebScan, doSingleScan: _rescanViewModel.doSingleScan, + hasDatePicker: !_rescanViewModel.isMwebScan,// disable date picker for mweb for now toggleSingleScan: () => _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, walletType: _rescanViewModel.wallet.type, diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 76c6c5cfd..2acf6138e 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; @@ -134,6 +135,10 @@ class RootState extends State with WidgetsBindingObserver { setState(() => _setInactive(true)); } + if (widget.appStore.wallet?.type == WalletType.litecoin) { + widget.appStore.wallet?.stopSync(); + } + break; case AppLifecycleState.resumed: widget.authService.requireAuth().then((value) { @@ -143,6 +148,9 @@ class RootState extends State with WidgetsBindingObserver { }); } }); + if (widget.appStore.wallet?.type == WalletType.litecoin) { + widget.appStore.wallet?.startSync(); + } break; default: break; diff --git a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart index 611b2acb7..79f74065a 100644 --- a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart +++ b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart @@ -60,6 +60,11 @@ class _DesktopSettingsPageState extends State { return Container(); } + if (!widget.dashboardViewModel.hasMweb && + item.name(context) == S.of(context).litecoin_mweb_settings) { + return Container(); + } + final isLastTile = index == itemCount - 1; return SettingActionButton( isLastTile: isLastTile, diff --git a/lib/src/screens/settings/mweb_settings.dart b/lib/src/screens/settings/mweb_settings.dart new file mode 100644 index 000000000..88dc00f7c --- /dev/null +++ b/lib/src/screens/settings/mweb_settings.dart @@ -0,0 +1,51 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class MwebSettingsPage extends BasePage { + MwebSettingsPage(this._mwebSettingsViewModel); + + @override + String get title => S.current.litecoin_mweb_settings; + + final MwebSettingsViewModel _mwebSettingsViewModel; + + @override + Widget body(BuildContext context) { + return SingleChildScrollView( + child: Observer(builder: (_) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + SettingsSwitcherCell( + title: S.current.litecoin_mweb_display_card, + value: _mwebSettingsViewModel.mwebCardDisplay, + onValueChange: (_, bool value) { + _mwebSettingsViewModel.setMwebCardDisplay(value); + }, + ), + SettingsSwitcherCell( + title: S.current.litecoin_mweb_always_scan, + value: _mwebSettingsViewModel.mwebAlwaysScan, + onValueChange: (_, bool value) { + _mwebSettingsViewModel.setMwebAlwaysScan(value); + }, + ), + SettingsCellWithArrow( + title: S.current.litecoin_mweb_scanning, + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan), + ), + ], + ), + ); + }), + ); + } +} diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart index 60a23c99b..236d06f4e 100644 --- a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart @@ -103,40 +103,63 @@ class UnspentCoinsListItem extends StatelessWidget { ), maxLines: 1, ), - if (isChange) - Container( - height: 17, - padding: EdgeInsets.only(left: 6, right: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8.5)), - color: Colors.white), - alignment: Alignment.center, - child: Text( - S.of(context).unspent_change, - style: TextStyle( - color: itemColor, - fontSize: 7, - fontWeight: FontWeight.w600, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (isChange) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + S.of(context).unspent_change, + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), - if (isSilentPayment) - Container( - height: 17, - padding: EdgeInsets.only(left: 6, right: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8.5)), - color: Colors.white), - alignment: Alignment.center, - child: Text( - S.of(context).silent_payments, - style: TextStyle( - color: itemColor, - fontSize: 7, - fontWeight: FontWeight.w600, + if (address.toLowerCase().contains("mweb")) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + margin: EdgeInsets.only(left: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + "MWEB", + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), + if (isSilentPayment) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + S.of(context).silent_payments, + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), ], ), ), diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 40e191438..9d66c1789 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -18,6 +18,7 @@ class BlockchainHeightWidget extends StatefulWidget { this.onHeightOrDateEntered, this.hasDatePicker = true, this.isSilentPaymentsScan = false, + this.isMwebScan = false, this.toggleSingleScan, this.doSingleScan = false, this.bitcoinMempoolAPIEnabled, @@ -29,6 +30,7 @@ class BlockchainHeightWidget extends StatefulWidget { final FocusNode? focusNode; final bool hasDatePicker; final bool isSilentPaymentsScan; + final bool isMwebScan; final bool doSingleScan; final Future? bitcoinMempoolAPIEnabled; final Function()? toggleSingleScan; @@ -170,7 +172,9 @@ class BlockchainHeightState extends State { if (date != null) { int height; - if (widget.isSilentPaymentsScan) { + if (widget.isMwebScan) { + height = bitcoin!.getLitecoinHeightByDate(date: date); + } else if (widget.isSilentPaymentsScan) { height = await bitcoin!.getHeightByDate( date: date, bitcoinMempoolAPIEnabled: await widget.bitcoinMempoolAPIEnabled, diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index 4013dd79e..d9b545040 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -23,8 +23,8 @@ class DashBoardRoundedCardWidget extends StatelessWidget { final String subTitle; final Widget? hint; final SvgPicture? svgPicture; + final Widget? icon; final Image? image; - final Icon? icon; final double? customBorder; @override diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index 272ed57c2..a8a9558d5 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -18,6 +18,7 @@ class SettingActions { walletSettingAction, addressBookSettingAction, silentPaymentsSettingAction, + litecoinMwebSettingAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -30,6 +31,7 @@ class SettingActions { walletSettingAction, addressBookSettingAction, silentPaymentsSettingAction, + litecoinMwebSettingAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -46,6 +48,15 @@ class SettingActions { }, ); + static SettingActions litecoinMwebSettingAction = SettingActions._( + name: (context) => S.current.litecoin_mweb_settings, + image: 'assets/images/bitcoin_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.mwebSettings); + }, + ); + static SettingActions connectionSettingAction = SettingActions._( name: (context) => S.of(context).connection_sync, image: 'assets/images/nodes_menu.png', diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index debaeb07a..9f03c95c3 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -114,6 +114,10 @@ abstract class SettingsStoreBase with Store { required this.customBitcoinFeeRate, required this.silentPaymentsCardDisplay, required this.silentPaymentsAlwaysScan, + required this.mwebAlwaysScan, + required this.mwebCardDisplay, + required this.mwebEnabled, + required this.hasEnabledMwebBefore, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialWowneroTransactionPriority, @@ -555,6 +559,24 @@ abstract class SettingsStoreBase with Store { (bool silentPaymentsAlwaysScan) => _sharedPreferences.setBool( PreferencesKey.silentPaymentsAlwaysScan, silentPaymentsAlwaysScan)); + reaction( + (_) => mwebAlwaysScan, + (bool mwebAlwaysScan) => + _sharedPreferences.setBool(PreferencesKey.mwebAlwaysScan, mwebAlwaysScan)); + + reaction( + (_) => mwebCardDisplay, + (bool mwebCardDisplay) => + _sharedPreferences.setBool(PreferencesKey.mwebCardDisplay, mwebCardDisplay)); + + reaction((_) => mwebEnabled, + (bool mwebEnabled) => _sharedPreferences.setBool(PreferencesKey.mwebEnabled, mwebEnabled)); + + reaction( + (_) => hasEnabledMwebBefore, + (bool hasEnabledMwebBefore) => + _sharedPreferences.setBool(PreferencesKey.hasEnabledMwebBefore, hasEnabledMwebBefore)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -768,6 +790,18 @@ abstract class SettingsStoreBase with Store { @observable bool silentPaymentsAlwaysScan; + @observable + bool mwebAlwaysScan; + + @observable + bool mwebCardDisplay; + + @observable + bool mwebEnabled; + + @observable + bool hasEnabledMwebBefore; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final BackgroundTasks _backgroundTasks; @@ -925,6 +959,11 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; final silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + final mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; + final mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; + final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; + final hasEnabledMwebBefore = + sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false; // If no value if (pinLength == null || pinLength == 0) { @@ -1191,6 +1230,10 @@ abstract class SettingsStoreBase with Store { customBitcoinFeeRate: customBitcoinFeeRate, silentPaymentsCardDisplay: silentPaymentsCardDisplay, silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, + mwebAlwaysScan: mwebAlwaysScan, + mwebCardDisplay: mwebCardDisplay, + mwebEnabled: mwebEnabled, + hasEnabledMwebBefore: hasEnabledMwebBefore, initialMoneroTransactionPriority: moneroTransactionPriority, initialWowneroTransactionPriority: wowneroTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority, @@ -1353,6 +1396,10 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; + mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; + mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; + hasEnabledMwebBefore = sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false; final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final bitcoinElectrumServerId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index e3a571eea..91797c45e 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -83,7 +83,7 @@ class ExceptionHandler { } static void onError(FlutterErrorDetails errorDetails) async { - if (kDebugMode) { + if (kDebugMode || kProfileMode) { FlutterError.presentError(errorDetails); debugPrint(errorDetails.toString()); return; diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 045b55261..c3fb5718a 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -21,10 +21,14 @@ class BalanceRecord { const BalanceRecord( {required this.availableBalance, required this.additionalBalance, + required this.secondAvailableBalance, + required this.secondAdditionalBalance, required this.frozenBalance, required this.fiatAvailableBalance, required this.fiatAdditionalBalance, required this.fiatFrozenBalance, + required this.fiatSecondAvailableBalance, + required this.fiatSecondAdditionalBalance, required this.asset, required this.formattedAssetTitle}); final String fiatAdditionalBalance; @@ -33,6 +37,10 @@ class BalanceRecord { final String additionalBalance; final String availableBalance; final String frozenBalance; + final String secondAvailableBalance; + final String secondAdditionalBalance; + final String fiatSecondAdditionalBalance; + final String fiatSecondAvailableBalance; final CryptoCurrency asset; final String formattedAssetTitle; } @@ -158,6 +166,26 @@ abstract class BalanceViewModelBase with Store { } } + @computed + String get secondAvailableBalanceLabel { + switch (wallet.type) { + case WalletType.litecoin: + return S.current.mweb_confirmed; + default: + return S.current.confirmed; + } + } + + @computed + String get secondAdditionalBalanceLabel { + switch (wallet.type) { + case WalletType.litecoin: + return S.current.mweb_unconfirmed; + default: + return S.current.unconfirmed; + } + } + @computed bool get hasMultiBalance => appStore.wallet!.type == WalletType.haven; @@ -243,9 +271,13 @@ abstract class BalanceViewModelBase with Store { availableBalance: '---', additionalBalance: '---', frozenBalance: '---', + secondAvailableBalance: '---', + secondAdditionalBalance: '---', fiatAdditionalBalance: isFiatDisabled ? '' : '---', fiatAvailableBalance: isFiatDisabled ? '' : '---', fiatFrozenBalance: isFiatDisabled ? '' : '---', + fiatSecondAvailableBalance: isFiatDisabled ? '' : '---', + fiatSecondAdditionalBalance: isFiatDisabled ? '' : '---', asset: key, formattedAssetTitle: _formatterAsset(key))); } @@ -274,24 +306,46 @@ abstract class BalanceViewModelBase with Store { ' ' + _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value))); + final secondAdditionalFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAdditionalBalance)); + + final secondAvailableFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAvailableBalance)); + return MapEntry( key, BalanceRecord( availableBalance: value.formattedAvailableBalance, additionalBalance: value.formattedAdditionalBalance, frozenBalance: getFormattedFrozenBalance(value), + secondAvailableBalance: value.formattedSecondAvailableBalance, + secondAdditionalBalance: value.formattedSecondAdditionalBalance, fiatAdditionalBalance: additionalFiatBalance, fiatAvailableBalance: availableFiatBalance, fiatFrozenBalance: frozenFiatBalance, + fiatSecondAvailableBalance: secondAvailableFiatBalance, + fiatSecondAdditionalBalance: secondAdditionalFiatBalance, asset: key, formattedAssetTitle: _formatterAsset(key))); }); } @computed - bool get hasAdditionalBalance => _hasAdditionBalanceForWalletType(wallet.type); + bool get hasAdditionalBalance => _hasAdditionalBalanceForWalletType(wallet.type); - bool _hasAdditionBalanceForWalletType(WalletType type) { + @computed + bool get hasSecondAdditionalBalance => _hasSecondAdditionalBalanceForWalletType(wallet.type); + + @computed + bool get hasSecondAvailableBalance => _hasSecondAvailableBalanceForWalletType(wallet.type); + + bool _hasAdditionalBalanceForWalletType(WalletType type) { switch (type) { case WalletType.ethereum: case WalletType.polygon: @@ -303,6 +357,20 @@ abstract class BalanceViewModelBase with Store { } } + bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { + if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) { + return true; + } + return false; + } + + bool _hasSecondAvailableBalanceForWalletType(WalletType type) { + if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) { + return true; + } + return false; + } + @computed List get formattedBalances { final balance = balances.values.toList(); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 4c3a9e1ea..21a167e2a 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -137,8 +137,8 @@ abstract class DashboardViewModelBase with Store { FilterItem( value: () => tradeFilterStore.displayLetsExchange, caption: ExchangeProviderDescription.letsExchange.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)), + onChanged: () => tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.letsExchange)), FilterItem( value: () => tradeFilterStore.displayStealthEx, caption: ExchangeProviderDescription.stealthEx.title, @@ -255,6 +255,16 @@ abstract class DashboardViewModelBase with Store { silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet); }); } + + if (hasMweb) { + mwebScanningActive = bitcoin!.getMwebEnabled(wallet); + settingsStore.mwebEnabled = mwebScanningActive; + reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) { + if (alwaysScan) { + mwebScanningActive = true; + } + }); + } } @observable @@ -348,6 +358,7 @@ abstract class DashboardViewModelBase with Store { bool get hasRescan => wallet.type == WalletType.bitcoin || wallet.type == WalletType.monero || + wallet.type == WalletType.litecoin || wallet.type == WalletType.wownero || wallet.type == WalletType.haven; @@ -416,6 +427,33 @@ abstract class DashboardViewModelBase with Store { } } + @computed + bool get hasMweb => wallet.type == WalletType.litecoin; + + @computed + bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay; + + @observable + bool mwebScanningActive = false; + + @computed + bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; + + @action + void setMwebScanningActive(bool active) { + if (!hasMweb) { + return; + } + + if (active) { + settingsStore.hasEnabledMwebBefore = true; + } + + settingsStore.mwebEnabled = active; + mwebScanningActive = active; + bitcoin!.setMwebEnabled(wallet, active); + } + BalanceViewModel balanceViewModel; AppStore appStore; diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 1e4726eee..744e4c58d 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -11,19 +11,31 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi abstract class ReceiveOptionViewModelBase with Store { ReceiveOptionViewModelBase(this._wallet, this.initialPageOption) : selectedReceiveOption = initialPageOption ?? - (_wallet.type == WalletType.bitcoin + (_wallet.type == WalletType.bitcoin || + _wallet.type == WalletType.litecoin ? bitcoin!.getSelectedAddressType(_wallet) : ReceivePageOption.mainnet), _options = [] { final walletType = _wallet.type; - _options = walletType == WalletType.haven - ? [ReceivePageOption.mainnet] - : walletType == WalletType.bitcoin - ? [ - ...bitcoin!.getBitcoinReceivePageOptions(), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ] - : ReceivePageOptions; + switch (walletType) { + case WalletType.bitcoin: + _options = [ + ...bitcoin!.getBitcoinReceivePageOptions(), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + break; + case WalletType.litecoin: + _options = [ + ...bitcoin!.getLitecoinReceivePageOptions(), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + break; + case WalletType.haven: + _options = [ReceivePageOption.mainnet]; + break; + default: + _options = ReceivePageOptions; + } } final WalletBase _wallet; diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 3101807b7..47fc32ab6 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -56,7 +56,8 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedPendingStatus { - if (balanceViewModel.wallet.type == WalletType.monero || balanceViewModel.wallet.type == WalletType.haven) { + if (balanceViewModel.wallet.type == WalletType.monero || + balanceViewModel.wallet.type == WalletType.haven) { if (transaction.confirmations >= 0 && transaction.confirmations < 10) { return ' (${transaction.confirmations}/10)'; } @@ -77,6 +78,13 @@ class TransactionListItem extends ActionListItem with Keyable { return transaction.isPending ? S.current.pending : ''; } + String get formattedType { + if (transaction.evmSignatureName == 'approval') { + return ' (${transaction.evmSignatureName})'; + } + return ''; + } + CryptoCurrency? get assetOfTransaction { try { if (balanceViewModel.wallet.type == WalletType.ethereum) { diff --git a/lib/view_model/rescan_view_model.dart b/lib/view_model/rescan_view_model.dart index 00eed5633..7ef1f6ba5 100644 --- a/lib/view_model/rescan_view_model.dart +++ b/lib/view_model/rescan_view_model.dart @@ -30,6 +30,8 @@ abstract class RescanViewModelBase with Store { bool get isSilentPaymentsScan => wallet.type == WalletType.bitcoin; @computed + bool get isMwebScan => wallet.type == WalletType.litecoin; + Future get isBitcoinMempoolAPIEnabled async => wallet.type == WalletType.bitcoin && await bitcoin!.checkIfMempoolAPIIsEnabled(wallet); diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index c1ab9d822..79f473eb3 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -217,7 +217,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor PendingTransaction? pendingTransaction; @computed - String get balance => wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance; + String get balance => wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance; @computed bool get isFiatDisabled => balanceViewModel.isFiatDisabled; @@ -675,6 +675,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return S.current.tx_no_dust_exception; } if (error is TransactionCommitFailed) { + if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { + return S.current.tx_commit_failed_no_peers; + } return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; } if (error is TransactionCommitFailedDustChange) { diff --git a/lib/view_model/settings/mweb_settings_view_model.dart b/lib/view_model/settings/mweb_settings_view_model.dart new file mode 100644 index 000000000..343947d00 --- /dev/null +++ b/lib/view_model/settings/mweb_settings_view_model.dart @@ -0,0 +1,32 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:mobx/mobx.dart'; + +part 'mweb_settings_view_model.g.dart'; + +class MwebSettingsViewModel = MwebSettingsViewModelBase with _$MwebSettingsViewModel; + +abstract class MwebSettingsViewModelBase with Store { + MwebSettingsViewModelBase(this._settingsStore, this._wallet); + + final SettingsStore _settingsStore; + final WalletBase _wallet; + + @computed + bool get mwebCardDisplay => _settingsStore.mwebCardDisplay; + + @computed + bool get mwebAlwaysScan => _settingsStore.mwebAlwaysScan; + + @action + void setMwebCardDisplay(bool value) { + _settingsStore.mwebCardDisplay = value; + } + + @action + void setMwebAlwaysScan(bool value) { + _settingsStore.mwebAlwaysScan = value; + bitcoin!.setMwebEnabled(_wallet, value); + } +} diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index e2d8469f1..72dcdb27b 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -38,6 +38,10 @@ abstract class UnspentCoinsListViewModelBase with Store { final info = getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage); + if (info == null) { + return; + } + info.isFrozen = item.isFrozen; info.isSending = item.isSending; info.note = item.note; @@ -50,15 +54,21 @@ abstract class UnspentCoinsListViewModelBase with Store { } } - UnspentCoinsInfo getUnspentCoinInfo( - String hash, String address, int value, int vout, String? keyImage) => - _unspentCoinsInfo.values.firstWhere((element) => + UnspentCoinsInfo? getUnspentCoinInfo( + String hash, String address, int value, int vout, String? keyImage) { + try { + return _unspentCoinsInfo.values.firstWhere((element) => element.walletId == wallet.id && element.hash == hash && element.address == address && element.value == value && element.vout == vout && element.keyImage == keyImage); + } catch (e) { + print("UnspentCoinsInfo not found for coin: $e"); + return null; + } + } String formatAmountToString(int fullBalance) { if (wallet.type == WalletType.monero) @@ -85,11 +95,18 @@ abstract class UnspentCoinsListViewModelBase with Store { } List _getUnspents() { - if (wallet.type == WalletType.monero) return monero!.getUnspents(wallet); - if (wallet.type == WalletType.wownero) return wownero!.getUnspents(wallet); - if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) - return bitcoin!.getUnspents(wallet); - return List.empty(); + switch (wallet.type) { + case WalletType.monero: + return monero!.getUnspents(wallet); + case WalletType.wownero: + return wownero!.getUnspents(wallet); + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + return bitcoin!.getUnspents(wallet); + default: + return List.empty(); + } } @action @@ -97,10 +114,13 @@ abstract class UnspentCoinsListViewModelBase with Store { _items.clear(); List unspents = []; - _getUnspents().forEach((elem) { + _getUnspents().forEach((Unspent elem) { try { final info = getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); + if (info == null) { + return; + } unspents.add(UnspentCoinsItem( address: elem.address, diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 0bd936720..17fc4b849 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; @@ -217,8 +219,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), - hasAccounts = - appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.wownero || appStore.wallet!.type == WalletType.haven, + hasAccounts = appStore.wallet!.type == WalletType.monero || + appStore.wallet!.type == WalletType.wownero || + appStore.wallet!.type == WalletType.haven, amount = '', _settingsStore = appStore.settingsStore, super(appStore: appStore) { @@ -230,7 +233,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven; + hasAccounts = wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || + wallet.type == WalletType.haven; } static const String _cryptoNumberPattern = '0.00000000'; @@ -404,7 +409,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }); addressList.addAll(receivedAddressItems); } else { - final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { + var addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { final isPrimary = subaddress.id == 0; return WalletAddressListItem( @@ -417,6 +422,16 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo walletTypeToCryptoCurrency(type), subaddress.balance), isChange: subaddress.isChange); }); + + // don't show all 1000+ mweb addresses: + if (wallet.type == WalletType.litecoin && addressItems.length >= 1000) { + // find the index of the last item with a txCount > 0 + final addressItemsList = addressItems.toList(); + final lastItemWithTxCount = addressItemsList.lastWhere((item) => (item.txCount ?? 0) > 0); + final index = addressItemsList.indexOf(lastItemWithTxCount); + // show only up to that index + 20: + addressItems = addressItemsList.sublist(0, index + 20); + } addressList.addAll(addressItems); } } @@ -446,7 +461,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo address: wallet.walletAddresses.address, )); } - + if (wallet.type == WalletType.tron) { final primaryAddress = tron!.getAddress(wallet); @@ -519,7 +534,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action Future setAddressType(dynamic option) async { - if (wallet.type == WalletType.bitcoin) { + if (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) { await bitcoin!.setAddressType(wallet, option); } } @@ -527,7 +542,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void _init() { _baseItems = []; - if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) { + if (wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || + wallet.type == WalletType.haven) { _baseItems.add(WalletAccountListHeader()); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0b4ee9415..86b3462ac 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import connectivity_plus +import cw_mweb import device_info_plus import devicelocale import flutter_inappwebview_macos @@ -21,6 +22,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) diff --git a/model_generator.sh b/model_generator.sh index 8a1173f7d..293923d1e 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -11,4 +11,5 @@ cd cw_tron; flutter pub get; flutter packages pub run build_runner build --delet cd cw_wownero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. cd cw_polygon; flutter pub get; cd .. cd cw_ethereum; flutter pub get; cd .. +cd cw_mweb && flutter pub get && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 98661f7c1..a2f19e596 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -97,10 +97,6 @@ dependencies: polyseed: ^0.0.6 nostr_tools: ^1.0.9 solana: ^0.30.1 - bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v7 ledger_flutter: ^1.0.1 hashlib: ^1.19.2 @@ -138,10 +134,12 @@ dependency_overrides: url: https://github.com/cake-tech/web3dart.git ref: cake flutter_secure_storage_platform_interface: 1.0.2 + protobuf: ^3.1.0 bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v7 + ref: cake-update-v8 + ffi: 2.1.0 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 48d744f08..e81696b2a 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "يرجى التأكد", "ledger_please_enable_bluetooth": "يرجى تمكين البلوتوث للكشف عن دفتر الأستاذ الخاص بك", "light_theme": "فاتح", + "litecoin_enable_mweb_sync": "تمكين MWEB المسح الضوئي", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "اضبط MWEB دائمًا على المسح الضوئي", + "litecoin_mweb_display_card": "عرض بطاقة mweb", + "litecoin_mweb_scanning": "MWEB المسح الضوئي", + "litecoin_mweb_settings": "إعدادات MWEB", + "litecoin_mweb_warning": "سيقوم استخدام MWEB في البداية بتنزيل ~ 600 ميجابايت من البيانات ، وقد يستغرق ما يصل إلى 30 دقيقة حسب سرعة الشبكة. سيتم تنزيل هذه البيانات الأولية مرة واحدة فقط وستكون متاحة لجميع محافظ Litecoin", + "litecoin_what_is_mweb": "ما هو MWEB؟", "live_fee_rates": "أسعار الرسوم المباشرة عبر API", "load_more": "تحميل المزيد", "loading_your_wallet": "يتم تحميل محفظتك", @@ -393,6 +401,8 @@ "monero_light_theme": " ضوء مونيرو", "moonpay_alert_text": "يجب أن تكون قيمة المبلغ أكبر من أو تساوي ${minAmount} ${fiatCurrency}", "more_options": "المزيد من الخيارات", + "mweb_confirmed": "أكد MWEB", + "mweb_unconfirmed": "غير مؤكد MWEB", "name": "ﻢﺳﺍ", "nano_current_rep": "الممثل الحالي", "nano_gpt_thanks_message": "شكرا لاستخدام nanogpt! تذكر أن تعود إلى المتصفح بعد اكتمال معاملتك!", @@ -814,6 +824,7 @@ "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": "لا يكفي المدخلات المتاحة. الرجاء تحديد المزيد تحت التحكم في العملة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 14cce0cfb..1ddcf99f9 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Моля, уверете се, че сте отворили правилното приложение на вашата книга", "ledger_please_enable_bluetooth": "Моля, активирайте Bluetooth да открие вашата книга", "light_theme": "Светло", + "litecoin_enable_mweb_sync": "Активирайте сканирането на MWeb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Задайте MWeb винаги сканиране", + "litecoin_mweb_display_card": "Показване на MWEB карта", + "litecoin_mweb_scanning": "Сканиране на MWEB", + "litecoin_mweb_settings": "Настройки на MWEB", + "litecoin_mweb_warning": "Използването на MWEB първоначално ще изтегли ~ 600MB данни и може да отнеме до 30 минути в зависимост от скоростта на мрежата. Тези първоначални данни ще изтеглят само веднъж и ще бъдат достъпни за всички портфейли Litecoin", + "litecoin_what_is_mweb": "Какво е MWEB?", "live_fee_rates": "Цени на таксите на живо чрез API", "load_more": "Зареди още", "loading_your_wallet": "Зареждане на портфейл", @@ -393,6 +401,8 @@ "monero_light_theme": "Лека тема Monero", "moonpay_alert_text": "Сумата трябва да бъде най-малко ${minAmount} ${fiatCurrency}", "more_options": "Още настройки", + "mweb_confirmed": "Потвърден MWeb", + "mweb_unconfirmed": "Непотвърден mweb", "name": "Име", "nano_current_rep": "Настоящ представител", "nano_gpt_thanks_message": "Благодаря, че използвахте Nanogpt! Не забравяйте да се върнете обратно към браузъра, след като транзакцията ви приключи!", @@ -814,6 +824,7 @@ "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": "Няма достатъчно налични входове. Моля, изберете повече под контрол на монети", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 344c5bf3b..f295692b1 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Ujistěte se, že se na své knize otevřete správnou aplikaci", "ledger_please_enable_bluetooth": "Umožněte prosím Bluetooth detekovat vaši knihu", "light_theme": "Světlý", + "litecoin_enable_mweb_sync": "Povolit skenování MWeb", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Nastavit MWeb vždy skenování", + "litecoin_mweb_display_card": "Zobrazit kartu MWeb", + "litecoin_mweb_scanning": "Skenování mWeb", + "litecoin_mweb_settings": "Nastavení mWeb", + "litecoin_mweb_warning": "Pomocí MWeb zpočátku stahuje ~ 600 MB dat a může trvat až 30 minut v závislosti na rychlosti sítě. Tato počáteční data si stáhnou pouze jednou a budou k dispozici pro všechny litecoinové peněženky", + "litecoin_what_is_mweb": "Co je Mweb?", "live_fee_rates": "Živé sazby poplatků prostřednictvím API", "load_more": "Načíst další", "loading_your_wallet": "Načítám peněženku", @@ -393,6 +401,8 @@ "monero_light_theme": "Světlé téma Monero", "moonpay_alert_text": "Částka musí být větší nebo rovna ${minAmount} ${fiatCurrency}", "more_options": "Více možností", + "mweb_confirmed": "Potvrzený mweb", + "mweb_unconfirmed": "Nepotvrzené mWeb", "name": "název", "nano_current_rep": "Současný zástupce", "nano_gpt_thanks_message": "Děkujeme za používání Nanogpt! Nezapomeňte se po dokončení transakce vydat zpět do prohlížeče!", @@ -814,6 +824,7 @@ "trusted": "Důvěřovat", "tx_commit_exception_no_dust_on_change": "Transakce je zamítnuta s touto částkou. S těmito mincemi můžete odeslat ${min} bez změny nebo ${max}, které se vrátí změna.", "tx_commit_failed": "Transakce COMPORT selhala. Kontaktujte prosím podporu.", + "tx_commit_failed_no_peers": "Transakce se nepodařilo vysílat, zkuste to prosím znovu za vteřinu", "tx_invalid_input": "Pro tento typ platby používáte nesprávný typ vstupu", "tx_no_dust_exception": "Transakce je zamítnuta odesláním příliš malé. Zkuste prosím zvýšit částku.", "tx_not_enough_inputs_exception": "Není k dispozici dostatek vstupů. Vyberte prosím více pod kontrolou mincí", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 6eb6ea3c3..acb5aa919 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Bitte stellen Sie sicher, dass Sie die richtige App auf Ihrem Ledger geöffnet haben", "ledger_please_enable_bluetooth": "Bitte aktivieren Sie Bluetooth um sich mit Ihren Ledger zu verbinden.", "light_theme": "Hell", + "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB -Scannen", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen", + "litecoin_mweb_display_card": "MWEB -Karte anzeigen", + "litecoin_mweb_scanning": "MWEB Scanning", + "litecoin_mweb_settings": "MWEB -Einstellungen", + "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin -Brieftaschen verfügbar", + "litecoin_what_is_mweb": "Was ist MWeb?", "live_fee_rates": "Live -Gebührenpreise über API", "load_more": "Mehr laden", "loading_your_wallet": "Wallet wird geladen", @@ -393,6 +401,8 @@ "monero_light_theme": "Monero Light-Thema", "moonpay_alert_text": "Der Wert des Betrags muss größer oder gleich ${minAmount} ${fiatCurrency} sein", "more_options": "Weitere Optionen", + "mweb_confirmed": "Bestätigt MWeb", + "mweb_unconfirmed": "Unbestätigter MWeb", "name": "Name", "nano_current_rep": "Aktueller Vertreter", "nano_gpt_thanks_message": "Danke, dass du Nanogpt benutzt hast! Denken Sie daran, nach Abschluss Ihrer Transaktion zurück zum Browser zu gehen!", @@ -815,6 +825,7 @@ "trusted": "Vertrauenswürdige", "tx_commit_exception_no_dust_on_change": "Die Transaktion wird diesen Betrag abgelehnt. Mit diesen Münzen können Sie ${min} ohne Veränderung oder ${max} senden, die Änderungen zurückgeben.", "tx_commit_failed": "Transaktionsausschüsse ist fehlgeschlagen. Bitte wenden Sie sich an Support.", + "tx_commit_failed_no_peers": "Transaktion konnte nicht übertragen werden. Bitte versuchen Sie es in einer Sekunde oder so erneut", "tx_invalid_input": "Sie verwenden den falschen Eingangstyp für diese Art von Zahlung", "tx_no_dust_exception": "Die Transaktion wird abgelehnt, indem eine Menge zu klein gesendet wird. Bitte versuchen Sie, die Menge zu erhöhen.", "tx_not_enough_inputs_exception": "Nicht genügend Eingänge verfügbar. Bitte wählen Sie mehr unter Münzkontrolle aus", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index c95816b28..0cb4b2f11 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Please make sure you opend the right app on your ledger", "ledger_please_enable_bluetooth": "Please enable Bluetooth to detect your Ledger", "light_theme": "Light", + "litecoin_enable_mweb_sync": "Enable MWEB scanning", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "Set MWEB always scanning", + "litecoin_mweb_display_card": "Show MWEB card", + "litecoin_mweb_scanning": "MWEB Scanning", + "litecoin_mweb_settings": "MWEB settings", + "litecoin_mweb_warning": "Using MWEB will initially download ~600MB of data, and may take up to 30 minutes depending on network speed. This initial data will only download once and be available for all Litecoin wallets", + "litecoin_what_is_mweb": "What is MWEB?", "live_fee_rates": "Live fee rates via API", "load_more": "Load more", "loading_your_wallet": "Loading your wallet", @@ -393,6 +401,8 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "Value of the amount must be more or equal to ${minAmount} ${fiatCurrency}", "more_options": "More Options", + "mweb_confirmed": "Confirmed MWEB", + "mweb_unconfirmed": "Unconfirmed MWEB", "name": "Name", "nano_current_rep": "Current Representative", "nano_gpt_thanks_message": "Thanks for using NanoGPT! Remember to head back to the browser after your transaction completes!", @@ -814,6 +824,7 @@ "trusted": "Trusted", "tx_commit_exception_no_dust_on_change": "The transaction is rejected with this amount. With these coins you can send ${min} without change or ${max} that returns change.", "tx_commit_failed": "Transaction commit failed. Please contact support.", + "tx_commit_failed_no_peers": "Transaction failed to broadcast, please try again in a second or so", "tx_invalid_input": "You are using the wrong input type for this type of payment", "tx_no_dust_exception": "The transaction is rejected by sending an amount too small. Please try increasing the amount.", "tx_not_enough_inputs_exception": "Not enough inputs available. Please select more under Coin Control", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 57cadbd08..d9b2db88a 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Por favor, asegúrese de abrir la aplicación correcta en su libro mayor.", "ledger_please_enable_bluetooth": "Habilite Bluetooth para detectar su libro mayor", "light_theme": "Ligera", + "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Establecer mweb siempre escaneo", + "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", + "litecoin_mweb_scanning": "Escaneo mweb", + "litecoin_mweb_settings": "Configuración de MWEB", + "litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin", + "litecoin_what_is_mweb": "¿Qué es mweb?", "live_fee_rates": "Tasas de tarifas en vivo a través de API", "load_more": "Carga más", "loading_your_wallet": "Cargando tu billetera", @@ -393,6 +401,8 @@ "monero_light_theme": "Tema ligero de Monero", "moonpay_alert_text": "El valor de la cantidad debe ser mayor o igual a ${minAmount} ${fiatCurrency}", "more_options": "Más Opciones", + "mweb_confirmed": "Confirmado mweb", + "mweb_unconfirmed": "Mweb no confirmado", "name": "Nombre", "nano_current_rep": "Representante actual", "nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerde regresar al navegador después de que se complete su transacción!", @@ -815,6 +825,7 @@ "trusted": "de confianza", "tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.", "tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.", + "tx_commit_failed_no_peers": "La transacción no se transmitió, intente nuevamente en un segundo más o menos", "tx_invalid_input": "Está utilizando el tipo de entrada incorrecto para este tipo de pago", "tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.", "tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 2de3c6d03..907058095 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Veuillez vous assurer d'ouvrir la bonne application sur votre grand livre", "ledger_please_enable_bluetooth": "Veuillez activer Bluetooth pour détecter votre grand livre", "light_theme": "Clair", + "litecoin_enable_mweb_sync": "Activer la numérisation MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Définir MWEB Score Scanning", + "litecoin_mweb_display_card": "Afficher la carte MWeb", + "litecoin_mweb_scanning": "Scann mweb", + "litecoin_mweb_settings": "Paramètres MWEB", + "litecoin_mweb_warning": "L'utilisation de MWEB téléchargera initialement ~ 600 Mo de données et peut prendre jusqu'à 30 minutes en fonction de la vitesse du réseau. Ces données initiales ne téléchargeront qu'une seule fois et seront disponibles pour tous les portefeuilles litecoin", + "litecoin_what_is_mweb": "Qu'est-ce que MWEB?", "live_fee_rates": "Taux de frais en direct via l'API", "load_more": "Charger plus", "loading_your_wallet": "Chargement de votre portefeuille (wallet)", @@ -393,6 +401,8 @@ "monero_light_theme": "Thème de lumière Monero", "moonpay_alert_text": "Le montant doit être au moins égal à ${minAmount} ${fiatCurrency}", "more_options": "Plus d'options", + "mweb_confirmed": "Confirmé MWEB", + "mweb_unconfirmed": "Mweb non confirmé", "name": "Nom", "nano_current_rep": "Représentant actuel", "nano_gpt_thanks_message": "Merci d'avoir utilisé Nanogpt! N'oubliez pas de retourner au navigateur une fois votre transaction terminée!", @@ -814,6 +824,7 @@ "trusted": "de confiance", "tx_commit_exception_no_dust_on_change": "La transaction est rejetée avec ce montant. Avec ces pièces, vous pouvez envoyer ${min} sans changement ou ${max} qui renvoie le changement.", "tx_commit_failed": "La validation de la transaction a échoué. Veuillez contacter l'assistance.", + "tx_commit_failed_no_peers": "La transaction n'a pas été diffusée, veuillez réessayer dans une seconde environ", "tx_invalid_input": "Vous utilisez le mauvais type d'entrée pour ce type de paiement", "tx_no_dust_exception": "La transaction est rejetée en envoyant un montant trop faible. Veuillez essayer d'augmenter le montant.", "tx_not_enough_inputs_exception": "Pas assez d'entrées disponibles. Veuillez sélectionner plus sous Control Control", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 3adefbea0..28b698c30 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Da fatan za a tabbata kun yi amfani da app ɗin dama akan dillalarku", "ledger_please_enable_bluetooth": "Da fatan za a kunna Bluetooth don gano Ledger ɗinku", "light_theme": "Haske", + "litecoin_enable_mweb_sync": "Kunna binciken Mweb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Saita Mweb koyaushe", + "litecoin_mweb_display_card": "Nuna katin Mweb", + "litecoin_mweb_scanning": "Mweb scanning", + "litecoin_mweb_settings": "Saitunan Mweb", + "litecoin_mweb_warning": "Amfani da Mweb zai fara saukewa ~ 600MB na bayanai, kuma yana iya ɗaukar minti 30 dangane da saurin cibiyar sadarwa. Wannan bayanan farko zai saika saukarwa sau ɗaya kawai kuma a samu don duk wuraren shakatawa", + "litecoin_what_is_mweb": "Menene Mweb?", "live_fee_rates": "Kudin Kiɗa ta API", "load_more": "Like more", "loading_your_wallet": "Ana loda walat ɗin ku", @@ -393,6 +401,8 @@ "monero_light_theme": "Jigon Hasken Monero", "moonpay_alert_text": "Darajar adadin dole ne ya zama fiye ko daidai da ${minAmount} ${fiatCurrency}", "more_options": "Ƙarin Zaɓuɓɓuka", + "mweb_confirmed": "Tabbatar da Mweb", + "mweb_unconfirmed": "Myconfired", "name": "Suna", "nano_current_rep": "Wakilin Yanzu", "nano_gpt_thanks_message": "Na gode da amfani da Nanogpt! Ka tuna da komawa zuwa mai bincike bayan ma'amalar ka ta cika!", @@ -816,6 +826,7 @@ "trusted": "Amintacce", "tx_commit_exception_no_dust_on_change": "An ƙi ma'amala da wannan adadin. Tare da waɗannan tsabar kudi Zaka iya aika ${min}, ba tare da canji ba ko ${max} wanda ya dawo canzawa.", "tx_commit_failed": "Ma'amala ya kasa. Da fatan za a tuntuɓi goyan baya.", + "tx_commit_failed_no_peers": "Kasuwanci ya kasa watsa, don Allah sake gwadawa a cikin na biyu ko", "tx_invalid_input": "Kuna amfani da nau'in shigar da ba daidai ba don wannan nau'in biyan kuɗi", "tx_no_dust_exception": "An ƙi ma'amala ta hanyar aika adadin ƙarami. Da fatan za a gwada ƙara adadin.", "tx_not_enough_inputs_exception": "Bai isa ba hanyoyin da ake samu. Da fatan za selectiari a karkashin Kwarewar Coin", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 403acec3e..8b46685a8 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "कृपया सुनिश्चित करें कि आप अपने लेजर पर सही ऐप को खोलते हैं", "ledger_please_enable_bluetooth": "कृपया अपने बहीखाने का पता लगाने के लिए ब्लूटूथ को सक्षम करें", "light_theme": "रोशनी", + "litecoin_enable_mweb_sync": "MWEB स्कैनिंग सक्षम करें", + "litecoin_mweb": "मावली", + "litecoin_mweb_always_scan": "MWEB हमेशा स्कैनिंग सेट करें", + "litecoin_mweb_display_card": "MWEB कार्ड दिखाएं", + "litecoin_mweb_scanning": "MWEB स्कैनिंग", + "litecoin_mweb_settings": "MWEB सेटिंग्स", + "litecoin_mweb_warning": "MWEB का उपयोग शुरू में ~ 600MB डेटा डाउनलोड करेगा, और नेटवर्क की गति के आधार पर 30 मिनट तक का समय लग सकता है। यह प्रारंभिक डेटा केवल एक बार डाउनलोड करेगा और सभी लिटकोइन वॉलेट के लिए उपलब्ध होगा", + "litecoin_what_is_mweb": "MWEB क्या है?", "live_fee_rates": "एपीआई के माध्यम से लाइव शुल्क दरें", "load_more": "और लोड करें", "loading_your_wallet": "अपना बटुआ लोड कर रहा है", @@ -393,6 +401,8 @@ "monero_light_theme": "मोनेरो लाइट थीम", "moonpay_alert_text": "राशि का मूल्य अधिक है या करने के लिए बराबर होना चाहिए ${minAmount} ${fiatCurrency}", "more_options": "और विकल्प", + "mweb_confirmed": "MWEB की पुष्टि की", + "mweb_unconfirmed": "अपुष्ट MWEB", "name": "नाम", "nano_current_rep": "वर्तमान प्रतिनिधि", "nano_gpt_thanks_message": "Nanogpt का उपयोग करने के लिए धन्यवाद! अपने लेन -देन के पूरा होने के बाद ब्राउज़र पर वापस जाना याद रखें!", @@ -816,6 +826,7 @@ "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": "पर्याप्त इनपुट उपलब्ध नहीं है। कृपया सिक्का नियंत्रण के तहत अधिक चुनें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index e225a11b8..ab3f536c7 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Obavezno obavezno otvorite pravu aplikaciju na knjizi", "ledger_please_enable_bluetooth": "Omogućite Bluetooth da otkrije svoju knjigu", "light_theme": "Svijetla", + "litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Postavite MWeb uvijek skeniranje", + "litecoin_mweb_display_card": "Prikaži MWeb karticu", + "litecoin_mweb_scanning": "MWEB skeniranje", + "litecoin_mweb_settings": "Postavke MWEB -a", + "litecoin_mweb_warning": "Korištenje MWEB -a u početku će preuzeti ~ 600MB podataka, a može potrajati do 30 minuta, ovisno o brzini mreže. Ovi početni podaci preuzet će samo jednom i biti dostupni za sve Litecoin novčanike", + "litecoin_what_is_mweb": "Što je MWEB?", "live_fee_rates": "Stope naknada uživo putem API -ja", "load_more": "Učitaj više", "loading_your_wallet": "Novčanik se učitava", @@ -393,6 +401,8 @@ "monero_light_theme": "Monero lagana tema", "moonpay_alert_text": "Vrijednost iznosa mora biti veća ili jednaka ${minAmount} ${fiatCurrency}", "more_options": "Više opcija", + "mweb_confirmed": "Potvrđen MWeb", + "mweb_unconfirmed": "Nepotvrđeni mWeb", "name": "Ime", "nano_current_rep": "Trenutni predstavnik", "nano_gpt_thanks_message": "Hvala što ste koristili nanogpt! Ne zaboravite da se vratite u preglednik nakon što vam se transakcija završi!", @@ -814,6 +824,7 @@ "trusted": "vjerovao", "tx_commit_exception_no_dust_on_change": "Transakcija se odbija s tim iznosom. Pomoću ovih kovanica možete poslati ${min} bez promjene ili ${max} koja vraća promjenu.", "tx_commit_failed": "Obveza transakcije nije uspjela. Molimo kontaktirajte podršku.", + "tx_commit_failed_no_peers": "Transakcija nije uspjela emitirati, pokušajte ponovo u sekundi ili tako", "tx_invalid_input": "Koristite pogrešnu vrstu ulaza za ovu vrstu plaćanja", "tx_no_dust_exception": "Transakcija se odbija slanjem iznosa premalo. Pokušajte povećati iznos.", "tx_not_enough_inputs_exception": "Nema dovoljno unosa. Molimo odaberite više pod kontrolom novčića", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index addddb34c..3d37a0a85 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -393,6 +393,8 @@ "monero_light_theme": "Monero պայծառ տեսք", "moonpay_alert_text": "Գումարի արժեքը պետք է լինի հավասար կամ ավելի քան ${minAmount} ${fiatCurrency}", "more_options": "Այլ տարբերակներ", + "mweb_confirmed": "Հաստատված MWEB", + "mweb_unconfirmed": "Չկարգավորված Mweb", "name": "Անուն", "nano_current_rep": "Ընթացիկ ներկայացուցիչ", "nano_gpt_thanks_message": "Շնորհակալություն NanoGPT-ն օգտագործելու համար: Հիշեք վերադառնալ դիտարկիչ ձեր փոխանցումն ավարտելուց հետո", @@ -814,6 +816,7 @@ "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": "Չկան բավարար մուտքեր: Խնդրում ենք ընտրել ավելին Coin Control֊ում", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 7848ab0ab..504c67d50 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Pastikan Anda membuka aplikasi yang tepat di buku besar Anda", "ledger_please_enable_bluetooth": "Harap aktifkan Bluetooth untuk mendeteksi buku besar Anda", "light_theme": "Terang", + "litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Atur mWeb selalu memindai", + "litecoin_mweb_display_card": "Tunjukkan kartu mWeb", + "litecoin_mweb_scanning": "Pemindaian MWEB", + "litecoin_mweb_settings": "Pengaturan MWEB", + "litecoin_mweb_warning": "Menggunakan MWEB pada awalnya akan mengunduh ~ 600MB data, dan dapat memakan waktu hingga 30 menit tergantung pada kecepatan jaringan. Data awal ini hanya akan mengunduh sekali dan tersedia untuk semua dompet litecoin", + "litecoin_what_is_mweb": "Apa itu MWEB?", "live_fee_rates": "Tarif biaya langsung melalui API", "load_more": "Muat lebih banyak", "loading_your_wallet": "Memuat dompet Anda", @@ -393,6 +401,8 @@ "monero_light_theme": "Tema Cahaya Monero", "moonpay_alert_text": "Nilai jumlah harus lebih atau sama dengan ${minAmount} ${fiatCurrency}", "more_options": "Opsi Lainnya", + "mweb_confirmed": "Mengkonfirmasi mWeb", + "mweb_unconfirmed": "MWEB yang belum dikonfirmasi", "name": "Nama", "nano_current_rep": "Perwakilan saat ini", "nano_gpt_thanks_message": "Terima kasih telah menggunakan Nanogpt! Ingatlah untuk kembali ke browser setelah transaksi Anda selesai!", @@ -817,6 +827,7 @@ "trusted": "Dipercayai", "tx_commit_exception_no_dust_on_change": "Transaksi ditolak dengan jumlah ini. Dengan koin ini Anda dapat mengirim ${min} tanpa perubahan atau ${max} yang mengembalikan perubahan.", "tx_commit_failed": "Transaksi Gagal. Silakan hubungi Dukungan.", + "tx_commit_failed_no_peers": "Transaksi gagal untuk disiarkan, silakan coba lagi sebentar lagi", "tx_invalid_input": "Anda menggunakan jenis input yang salah untuk jenis pembayaran ini", "tx_no_dust_exception": "Transaksi ditolak dengan mengirimkan jumlah yang terlalu kecil. Silakan coba tingkatkan jumlahnya.", "tx_not_enough_inputs_exception": "Tidak cukup input yang tersedia. Pilih lebih banyak lagi di bawah Kontrol Koin", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 6f40f0633..668346dfa 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -364,6 +364,14 @@ "ledger_error_wrong_app": "Assicurati di aprire l'app giusta sul libro mastro", "ledger_please_enable_bluetooth": "Si prega di consentire al Bluetooth di rilevare il libro mastro", "light_theme": "Bianco", + "litecoin_enable_mweb_sync": "Abilita la scansione MWeb", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Imposta MWeb per scansionare sempre", + "litecoin_mweb_display_card": "Mostra la scheda MWeb", + "litecoin_mweb_scanning": "Scansione MWeb", + "litecoin_mweb_settings": "Impostazioni MWeb", + "litecoin_mweb_warning": "L'uso di MWeb inizialmente scaricherà ~ 600 MB di dati e potrebbe richiedere fino a 30 minuti a seconda della velocità di rete. Questi dati iniziali scaricheranno solo una volta e saranno disponibili per tutti i portafogli Litecoin", + "litecoin_what_is_mweb": "Cos'è MWeb?", "live_fee_rates": "Tariffe delle commissioni dal vivo tramite API", "load_more": "Carica di più", "loading_your_wallet": "Caricamento portafoglio", @@ -394,6 +402,8 @@ "monero_light_theme": "Tema leggero Monero", "moonpay_alert_text": "Il valore dell'importo deve essere maggiore o uguale a ${minAmount} ${fiatCurrency}", "more_options": "Altre opzioni", + "mweb_confirmed": "MWeb confermato", + "mweb_unconfirmed": "MWeb non confermato", "name": "Nome", "nano_current_rep": "Rappresentante attuale", "nano_gpt_thanks_message": "Grazie per aver usato il nanogpt! Ricorda di tornare al browser dopo il completamento della transazione!", @@ -816,6 +826,7 @@ "trusted": "di fiducia", "tx_commit_exception_no_dust_on_change": "La transazione viene respinta con questo importo. Con queste monete è possibile inviare ${min} senza modifiche o ${max} che restituisce il cambiamento.", "tx_commit_failed": "Commit di transazione non riuscita. Si prega di contattare il supporto.", + "tx_commit_failed_no_peers": "La transazione non è riuscita a trasmettere, riprovare in un secondo o giù di lì", "tx_invalid_input": "Stai usando il tipo di input sbagliato per questo tipo di pagamento", "tx_no_dust_exception": "La transazione viene respinta inviando un importo troppo piccolo. Per favore, prova ad aumentare l'importo.", "tx_not_enough_inputs_exception": "Input non sufficienti disponibili. Seleziona di più sotto il controllo delle monete", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index a850b78c6..301b0f6cc 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -364,6 +364,14 @@ "ledger_error_wrong_app": "元帳に適切なアプリを開始するようにしてください", "ledger_please_enable_bluetooth": "Bluetoothが元帳を検出できるようにしてください", "light_theme": "光", + "litecoin_enable_mweb_sync": "MWEBスキャンを有効にします", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "MWEBを常にスキャンします", + "litecoin_mweb_display_card": "MWEBカードを表示します", + "litecoin_mweb_scanning": "MWEBスキャン", + "litecoin_mweb_settings": "MWEB設定", + "litecoin_mweb_warning": "MWEBを使用すると、最初は〜600MBのデータをダウンロードし、ネットワーク速度に応じて最大30分かかる場合があります。この最初のデータは一度だけダウンロードされ、すべてのLitecoinウォレットで利用可能になります", + "litecoin_what_is_mweb": "MWEBとは何ですか?", "live_fee_rates": "API経由のライブ料金", "load_more": "もっと読み込む", "loading_your_wallet": "ウォレットをロードしています", @@ -394,6 +402,8 @@ "monero_light_theme": "モネロ ライト テーマ", "moonpay_alert_text": "金額の値は以上でなければなりません ${minAmount} ${fiatCurrency}", "more_options": "その他のオプション", + "mweb_confirmed": "確認されたMWEB", + "mweb_unconfirmed": "未確認のMWEB", "name": "名前", "nano_current_rep": "現在の代表", "nano_gpt_thanks_message": "NanoGptを使用してくれてありがとう!トランザクションが完了したら、ブラウザに戻ることを忘れないでください!", @@ -815,6 +825,7 @@ "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": "利用可能な入力が十分ではありません。コイン制御下でもっと選択してください", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 86725aa5c..5ae26757c 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "원장에서 올바른 앱을 반대하는지 확인하십시오.", "ledger_please_enable_bluetooth": "Bluetooth가 원장을 감지 할 수 있도록하십시오", "light_theme": "빛", + "litecoin_enable_mweb_sync": "mweb 스캔을 활성화합니다", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "mweb는 항상 스캔을 설정합니다", + "litecoin_mweb_display_card": "mweb 카드를 보여주십시오", + "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": "지갑 넣기", @@ -393,6 +401,8 @@ "monero_light_theme": "모네로 라이트 테마", "moonpay_alert_text": "금액은 다음보다 크거나 같아야합니다 ${minAmount} ${fiatCurrency}", "more_options": "추가 옵션", + "mweb_confirmed": "확인 mweb", + "mweb_unconfirmed": "확인되지 않은 mweb", "name": "이름", "nano_current_rep": "현재 대표", "nano_gpt_thanks_message": "Nanogpt를 사용해 주셔서 감사합니다! 거래가 완료된 후 브라우저로 돌아가는 것을 잊지 마십시오!", @@ -815,6 +825,7 @@ "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": "사용 가능한 입력이 충분하지 않습니다. 코인 컨트롤에서 더 많은 것을 선택하십시오", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 588f8bf4e..b9c6441a7 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "ကျေးဇူးပြု. သင့်လက်ျာအက်ပ်ကိုသင်၏ Ledger တွင်ဖွင့်ရန်သေချာစေပါ", "ledger_please_enable_bluetooth": "သင်၏ Ledger ကိုရှာဖွေရန် Bluetooth ကိုဖွင့်ပါ", "light_theme": "အလင်း", + "litecoin_enable_mweb_sync": "mweb scanning ဖွင့်ပါ", + "litecoin_mweb": "မင်္ဂလာပါ", + "litecoin_mweb_always_scan": "Mweb အမြဲစကင်ဖတ်စစ်ဆေးပါ", + "litecoin_mweb_display_card": "MweB ကဒ်ကိုပြပါ", + "litecoin_mweb_scanning": "mweb scanning", + "litecoin_mweb_settings": "Mweb ဆက်တင်များ", + "litecoin_mweb_warning": "MweB ကိုအသုံးပြုခြင်းသည်အစပိုင်းတွင် ~ 600MB ဒေတာများကို download လုပ်ပြီးကွန်ယက်အမြန်နှုန်းပေါ် မူတည်. မိနစ် 30 အထိကြာနိုင်သည်။ ဤကန ဦး ဒေတာကိုတစ်ကြိမ်သာ download လုပ်ပြီး litecoin Walkets အားလုံးအတွက်ရနိုင်သည်", + "litecoin_what_is_mweb": "MweB ဆိုတာဘာလဲ။", "live_fee_rates": "API မှတစ်ဆင့် Live အခကြေးငွေနှုန်းထားများ", "load_more": "ပိုပြီး load", "loading_your_wallet": "သင့်ပိုက်ဆံအိတ်ကို ဖွင့်နေသည်။", @@ -393,6 +401,8 @@ "monero_light_theme": "Monero Light အပြင်အဆင်", "moonpay_alert_text": "ပမာဏ၏တန်ဖိုးသည် ${minAmount} ${fiatCurrency} နှင့် ပိုနေရမည်", "more_options": "နောက်ထပ် ရွေးချယ်စရာများ", + "mweb_confirmed": "အတည်ပြုလိုက် mweb", + "mweb_unconfirmed": "အတည်မပြုနိုင်သော mweb", "name": "နာမည်", "nano_current_rep": "လက်ရှိကိုယ်စားလှယ်", "nano_gpt_thanks_message": "nanogpt ကိုသုံးပြီးကျေးဇူးတင်ပါတယ် သင်၏ငွေပေးငွေယူပြီးနောက် browser သို့ပြန်သွားရန်သတိရပါ။", @@ -814,6 +824,7 @@ "trusted": "ယုံတယ်။", "tx_commit_exception_no_dust_on_change": "အဆိုပါငွေပေးငွေယူကဒီပမာဏနှင့်အတူပယ်ချခံရသည်။ ဤဒင်္ဂါးပြားများနှင့်အတူပြောင်းလဲမှုကိုပြန်လည်ပြောင်းလဲခြင်းသို့မဟုတ် ${min} မပါဘဲ ${max} ပေးပို့နိုင်သည်။", "tx_commit_failed": "ငွေပေးငွေယူကျူးလွန်မှုပျက်ကွက်။ ကျေးဇူးပြုပြီးပံ့ပိုးမှုဆက်သွယ်ပါ။", + "tx_commit_failed_no_peers": "ငွေပေးငွေယူထုတ်လွှင့်ရန်ပျက်ကွက်ပါက ကျေးဇူးပြု. ဒုတိယသို့မဟုတ်ထိုအတိုင်းထပ်မံကြိုးစားပါ", "tx_invalid_input": "သင်သည်ဤငွေပေးချေမှုအမျိုးအစားအတွက်မှားယွင်းသော input type ကိုအသုံးပြုနေသည်", "tx_no_dust_exception": "ငွေပမာဏကိုသေးငယ်လွန်းသောငွေပမာဏကိုပေးပို့ခြင်းဖြင့်ပယ်ဖျက်ခြင်းကိုငြင်းပယ်သည်။ ကျေးဇူးပြုပြီးငွေပမာဏကိုတိုးမြှင့်ကြိုးစားပါ။", "tx_not_enough_inputs_exception": "အလုံအလောက်သွင်းအားစုများမလုံလောက်။ ကျေးဇူးပြုပြီးဒင်္ဂါးပြားထိန်းချုပ်မှုအောက်တွင်ပိုမိုရွေးချယ်ပါ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 38bf2d69f..f447b9a3a 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Zorg ervoor dat u de juiste app op uw grootboek opent", "ledger_please_enable_bluetooth": "Schakel Bluetooth in staat om uw grootboek te detecteren", "light_theme": "Licht", + "litecoin_enable_mweb_sync": "MWEB -scanning inschakelen", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Stel mweb altijd op scannen", + "litecoin_mweb_display_card": "Toon MWEB -kaart", + "litecoin_mweb_scanning": "MWEB -scanning", + "litecoin_mweb_settings": "MWEB -instellingen", + "litecoin_mweb_warning": "Het gebruik van MWeb downloadt in eerste instantie ~ 600 MB aan gegevens en kan tot 30 minuten duren, afhankelijk van de netwerksnelheid. Deze eerste gegevens worden slechts eenmaal gedownload en zijn beschikbaar voor alle Litecoin -portefeuilles", + "litecoin_what_is_mweb": "Wat is Mweb?", "live_fee_rates": "Live -tarieven via API", "load_more": "Meer laden", "loading_your_wallet": "Uw portemonnee laden", @@ -393,6 +401,8 @@ "monero_light_theme": "Monero Light-thema", "moonpay_alert_text": "Waarde van het bedrag moet meer of gelijk zijn aan ${minAmount} ${fiatCurrency}", "more_options": "Meer opties", + "mweb_confirmed": "Bevestigde MWEB", + "mweb_unconfirmed": "Onbevestigde MWEB", "name": "Naam", "nano_current_rep": "Huidige vertegenwoordiger", "nano_gpt_thanks_message": "Bedankt voor het gebruik van Nanogpt! Vergeet niet om terug te gaan naar de browser nadat uw transactie is voltooid!", @@ -814,6 +824,7 @@ "trusted": "vertrouwd", "tx_commit_exception_no_dust_on_change": "De transactie wordt afgewezen met dit bedrag. Met deze munten kunt u ${min} verzenden zonder verandering of ${max} die wijziging retourneert.", "tx_commit_failed": "Transactiebewissing is mislukt. Neem contact op met de ondersteuning.", + "tx_commit_failed_no_peers": "De transactie is niet uitgezonden, probeer het opnieuw binnen een seconde of zo", "tx_invalid_input": "U gebruikt het verkeerde invoertype voor dit type betaling", "tx_no_dust_exception": "De transactie wordt afgewezen door een te klein bedrag te verzenden. Probeer het bedrag te verhogen.", "tx_not_enough_inputs_exception": "Niet genoeg ingangen beschikbaar. Selecteer meer onder muntenbesturing", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index fc776a1af..bcd420679 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Upewnij się, że opisz odpowiednią aplikację na swojej księdze", "ledger_please_enable_bluetooth": "Włącz Bluetooth wykrywanie księgi", "light_theme": "Jasny", + "litecoin_enable_mweb_sync": "Włącz skanowanie MWEB", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "Ustaw MWEB zawsze skanowanie", + "litecoin_mweb_display_card": "Pokaż kartę MWEB", + "litecoin_mweb_scanning": "Skanowanie MWEB", + "litecoin_mweb_settings": "Ustawienia MWEB", + "litecoin_mweb_warning": "Korzystanie z MWEB początkowo pobiera ~ 600 MB danych i może potrwać do 30 minut w zależności od prędkości sieci. Te początkowe dane pobierają tylko raz i będą dostępne dla wszystkich portfeli Litecoin", + "litecoin_what_is_mweb": "Co to jest MWEB?", "live_fee_rates": "Stawki opłaty na żywo za pośrednictwem API", "load_more": "Załaduj więcej", "loading_your_wallet": "Ładowanie portfela", @@ -393,6 +401,8 @@ "monero_light_theme": "Lekki motyw Monero", "moonpay_alert_text": "Wartość kwoty musi być większa lub równa ${minAmount} ${fiatCurrency}", "more_options": "Więcej opcji", + "mweb_confirmed": "Potwierdził MWEB", + "mweb_unconfirmed": "Niepotwierdzone MWEB", "name": "Nazwa", "nano_current_rep": "Obecny przedstawiciel", "nano_gpt_thanks_message": "Dzięki za użycie Nanogpt! Pamiętaj, aby wrócić do przeglądarki po zakończeniu transakcji!", @@ -814,6 +824,7 @@ "trusted": "Zaufany", "tx_commit_exception_no_dust_on_change": "Transakcja jest odrzucana z tą kwotą. Za pomocą tych monet możesz wysłać ${min} bez zmiany lub ${max}, które zwraca zmianę.", "tx_commit_failed": "Zatwierdzenie transakcji nie powiodło się. Skontaktuj się z obsługą.", + "tx_commit_failed_no_peers": "Transakcja nie była transmitowana, spróbuj ponownie za około sekundę", "tx_invalid_input": "Używasz niewłaściwego typu wejściowego dla tego rodzaju płatności", "tx_no_dust_exception": "Transakcja jest odrzucana przez wysyłanie zbyt małej ilości. Spróbuj zwiększyć kwotę.", "tx_not_enough_inputs_exception": "Za mało dostępnych danych wejściowych. Wybierz więcej pod kontrolą monet", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 6a892cda0..51d11f9b0 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Por favor, certifique -se de optar pelo aplicativo certo no seu livro", "ledger_please_enable_bluetooth": "Ative o Bluetooth para detectar seu livro", "light_theme": "Luz", + "litecoin_enable_mweb_sync": "Ativar digitalização do MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Definir mweb sempre digitalizando", + "litecoin_mweb_display_card": "Mostre o cartão MWEB", + "litecoin_mweb_scanning": "MWEB Scanning", + "litecoin_mweb_settings": "Configurações do MWEB", + "litecoin_mweb_warning": "O uso do MWEB baixará inicialmente ~ 600 MB de dados e pode levar até 30 minutos, dependendo da velocidade da rede. Esses dados iniciais serão baixados apenas uma vez e estarão disponíveis para todas as carteiras Litecoin", + "litecoin_what_is_mweb": "O que é MWeb?", "live_fee_rates": "Taxas de taxas ao vivo via API", "load_more": "Carregue mais", "loading_your_wallet": "Abrindo sua carteira", @@ -394,6 +402,8 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "O valor do montante deve ser maior ou igual a ${minAmount} ${fiatCurrency}", "more_options": "Mais opções", + "mweb_confirmed": "MWEB confirmado", + "mweb_unconfirmed": "MWEB não confirmado", "name": "Nome", "nano_current_rep": "Representante atual", "nano_gpt_thanks_message": "Obrigado por usar o Nanogpt! Lembre -se de voltar para o navegador após a conclusão da transação!", @@ -816,6 +826,7 @@ "trusted": "confiável", "tx_commit_exception_no_dust_on_change": "A transação é rejeitada com esse valor. Com essas moedas, você pode enviar ${min} sem alteração ou ${max} que retorna alterações.", "tx_commit_failed": "A confirmação da transação falhou. Entre em contato com o suporte.", + "tx_commit_failed_no_peers": "A transação não foi transmitida, tente novamente em um segundo", "tx_invalid_input": "Você está usando o tipo de entrada errado para este tipo de pagamento", "tx_no_dust_exception": "A transação é rejeitada enviando uma quantia pequena demais. Por favor, tente aumentar o valor.", "tx_not_enough_inputs_exception": "Não há entradas disponíveis. Selecione mais sob controle de moedas", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index dd3ce9654..94c8a2de3 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Пожалуйста, убедитесь, что вы предлагаете правильное приложение в своей бухгалтерской книге", "ledger_please_enable_bluetooth": "Пожалуйста, включите Bluetooth обнаружить вашу бухгалтерскую книгу", "light_theme": "Светлая", + "litecoin_enable_mweb_sync": "Включить MWEB сканирование", + "litecoin_mweb": "Мвеб", + "litecoin_mweb_always_scan": "Установить MWEB всегда сканирование", + "litecoin_mweb_display_card": "Показать карту MWEB", + "litecoin_mweb_scanning": "MWEB сканирование", + "litecoin_mweb_settings": "Настройки MWEB", + "litecoin_mweb_warning": "Использование MWEB изначально загрузит ~ 600 МБ данных и может занять до 30 минут в зависимости от скорости сети. Эти начальные данные будут загружаться только один раз и будут доступны для всех кошельков Litecoin", + "litecoin_what_is_mweb": "Что такое MWEB?", "live_fee_rates": "Ставки по сбору вживую через API", "load_more": "Загрузи больше", "loading_your_wallet": "Загрузка кошелька", @@ -393,6 +401,8 @@ "monero_light_theme": "Светлая тема Monero", "moonpay_alert_text": "Сумма должна быть больше или равна ${minAmount} ${fiatCurrency}", "more_options": "Дополнительные параметры", + "mweb_confirmed": "Подтверждено MWEB", + "mweb_unconfirmed": "Неподтвержденная MWEB", "name": "Имя", "nano_current_rep": "Нынешний представитель", "nano_gpt_thanks_message": "Спасибо за использование Nanogpt! Не забудьте вернуться в браузер после завершения транзакции!", @@ -815,6 +825,7 @@ "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": "Недостаточно входов доступны. Пожалуйста, выберите больше под контролем монет", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index e13e6053e..8fb6b12cc 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "โปรดตรวจสอบให้แน่ใจว่าคุณเปิดแอพที่เหมาะสมในบัญชีแยกประเภทของคุณ", "ledger_please_enable_bluetooth": "โปรดเปิดใช้งานบลูทู ธ ในการตรวจจับบัญชีแยกประเภทของคุณ", "light_theme": "สว่าง", + "litecoin_enable_mweb_sync": "เปิดใช้งานการสแกน MWEB", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "ตั้งค่าการสแกน MWEB เสมอ", + "litecoin_mweb_display_card": "แสดงการ์ด mweb", + "litecoin_mweb_scanning": "การสแกน MWEB", + "litecoin_mweb_settings": "การตั้งค่า MWEB", + "litecoin_mweb_warning": "การใช้ MWEB จะดาวน์โหลดข้อมูล ~ 600MB ในขั้นต้นและอาจใช้เวลาสูงสุด 30 นาทีขึ้นอยู่กับความเร็วเครือข่าย ข้อมูลเริ่มต้นนี้จะดาวน์โหลดได้เพียงครั้งเดียวและพร้อมใช้งานสำหรับกระเป๋าเงินทั้งหมดของ Litecoin", + "litecoin_what_is_mweb": "MWEB คืออะไร?", "live_fee_rates": "อัตราค่าธรรมเนียมสดผ่าน API", "load_more": "โหลดมากขึ้น", "loading_your_wallet": "กำลังโหลดกระเป๋าของคุณ", @@ -393,6 +401,8 @@ "monero_light_theme": "ธีมแสง Monero", "moonpay_alert_text": "มูลค่าของจำนวนต้องมากกว่าหรือเท่ากับ ${minAmount} ${fiatCurrency}", "more_options": "ตัวเลือกเพิ่มเติม", + "mweb_confirmed": "MWEB ยืนยันแล้ว", + "mweb_unconfirmed": "mweb ที่ไม่ได้รับการยืนยัน", "name": "ชื่อ", "nano_current_rep": "ตัวแทนปัจจุบัน", "nano_gpt_thanks_message": "ขอบคุณที่ใช้ Nanogpt! อย่าลืมกลับไปที่เบราว์เซอร์หลังจากการทำธุรกรรมของคุณเสร็จสิ้น!", @@ -814,6 +824,7 @@ "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": "มีอินพุตไม่เพียงพอ โปรดเลือกเพิ่มเติมภายใต้การควบคุมเหรียญ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 3ec6160ed..1b699ba06 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Mangyaring tiyaking pinipili mo ang tamang app sa iyong Ledger", "ledger_please_enable_bluetooth": "Mangyaring paganahin ang Bluetooth upang makita ang iyong Ledger", "light_theme": "Light", + "litecoin_enable_mweb_sync": "Paganahin ang pag -scan ng MWeb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Itakda ang MWeb na laging nag -scan", + "litecoin_mweb_display_card": "Ipakita ang MWEB Card", + "litecoin_mweb_scanning": "Pag -scan ng Mweb", + "litecoin_mweb_settings": "Mga Setting ng Mweb", + "litecoin_mweb_warning": "Ang paggamit ng MWEB ay unang i -download ang ~ 600MB ng data, at maaaring tumagal ng hanggang sa 30 minuto depende sa bilis ng network. Ang paunang data na ito ay mag -download lamang ng isang beses at magagamit para sa lahat ng mga wallets ng Litecoin", + "litecoin_what_is_mweb": "Ano ang MWEB?", "live_fee_rates": "Mga rate ng live na bayad sa pamamagitan ng API", "load_more": "Mag-load pa", "loading_your_wallet": "Naglo-load ng iyong wallet", @@ -393,6 +401,8 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "Ang halaga ay dapat na higit pa o katumbas ng ${minAmount} ${fiatCurrency}", "more_options": "Higit pang mga Pagpipilian", + "mweb_confirmed": "Nakumpirma na MWeb", + "mweb_unconfirmed": "Hindi nakumpirma si Mweb", "name": "Pangalan", "nano_current_rep": "Kasalukuyang Representative", "nano_gpt_thanks_message": "Salamat sa paggamit ng NanoGPT! Tandaan na bumalik sa browser matapos makumpleto ang iyong transaksyon!", @@ -814,6 +824,7 @@ "trusted": "Pinagkakatiwalaan", "tx_commit_exception_no_dust_on_change": "Ang transaksyon ay tinanggihan sa halagang ito. Sa mga barya na ito maaari kang magpadala ng ${min} nang walang sukli o ${max} na nagbabalik ng sukli.", "tx_commit_failed": "Nabigo ang transaksyon. Mangyaring makipag-ugnay sa suporta.", + "tx_commit_failed_no_peers": "Nabigo ang transaksyon na mag -broadcast, mangyaring subukang muli sa isang segundo o higit pa", "tx_invalid_input": "Gumagamit ka ng maling uri ng pag-input para sa ganitong uri ng pagbabayad", "tx_no_dust_exception": "Ang transaksyon ay tinanggihan sa pamamagitan ng pagpapadala ng isang maliit na halaga. Mangyaring subukang dagdagan ang halaga.", "tx_not_enough_inputs_exception": "Hindi sapat na magagamit ang mga input. Mangyaring pumili ng higit pa sa ilalim ng Coin Control", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index ab2159c95..c352192a8 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Lütfen defterinizde doğru uygulamayı açtığınızdan emin olun", "ledger_please_enable_bluetooth": "Defterinizi algılamak için lütfen Bluetooth'u etkinleştirin", "light_theme": "Aydınlık", + "litecoin_enable_mweb_sync": "MWEB taramasını etkinleştir", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "MWEB'i her zaman taramayı ayarlayın", + "litecoin_mweb_display_card": "MWEB kartını göster", + "litecoin_mweb_scanning": "MWEB taraması", + "litecoin_mweb_settings": "MWEB ayarları", + "litecoin_mweb_warning": "MWEB kullanmak başlangıçta ~ 600MB veri indirir ve ağ hızına bağlı olarak 30 dakikaya kadar sürebilir. Bu ilk veriler yalnızca bir kez indirilecek ve tüm Litecoin cüzdanları için kullanılabilir olacak", + "litecoin_what_is_mweb": "MWEB nedir?", "live_fee_rates": "API üzerinden canlı ücret oranları", "load_more": "Daha fazla yükle", "loading_your_wallet": "Cüzdanın yükleniyor", @@ -393,6 +401,8 @@ "monero_light_theme": "Monero Hafif Tema", "moonpay_alert_text": "Tutar ${minAmount} ${fiatCurrency} miktarına eşit veya daha fazla olmalıdır", "more_options": "Daha Fazla Seçenek", + "mweb_confirmed": "Onaylanmış mweb", + "mweb_unconfirmed": "Doğrulanmamış mweb", "name": "İsim", "nano_current_rep": "Mevcut temsilci", "nano_gpt_thanks_message": "Nanogpt kullandığınız için teşekkürler! İşleminiz tamamlandıktan sonra tarayıcıya geri dönmeyi unutmayın!", @@ -814,6 +824,7 @@ "trusted": "Güvenilir", "tx_commit_exception_no_dust_on_change": "İşlem bu miktarla reddedilir. Bu madeni paralarla değişiklik yapmadan ${min} veya değişikliği döndüren ${max} gönderebilirsiniz.", "tx_commit_failed": "İşlem taahhüdü başarısız oldu. Lütfen Destek ile iletişime geçin.", + "tx_commit_failed_no_peers": "İşlem yayın yapamadı, lütfen bir saniye içinde tekrar deneyin", "tx_invalid_input": "Bu tür ödeme için yanlış giriş türünü kullanıyorsunuz", "tx_no_dust_exception": "İşlem, çok küçük bir miktar gönderilerek reddedilir. Lütfen miktarı artırmayı deneyin.", "tx_not_enough_inputs_exception": "Yeterli giriş yok. Lütfen madeni para kontrolü altında daha fazlasını seçin", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 1d6e05dee..bb8dce457 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "Будь ласка, переконайтеся, що ви відкриваєте потрібну програму на своїй книзі", "ledger_please_enable_bluetooth": "Будь ласка, ввімкніть Bluetooth виявити свою книгу", "light_theme": "Світла", + "litecoin_enable_mweb_sync": "Увімкнути сканування MWEB", + "litecoin_mweb": "Мвеб", + "litecoin_mweb_always_scan": "Встановити mweb завжди сканувати", + "litecoin_mweb_display_card": "Показати карту MWeb", + "litecoin_mweb_scanning": "Сканування Mweb", + "litecoin_mweb_settings": "Налаштування MWEB", + "litecoin_mweb_warning": "Використання MWEB спочатку завантажить ~ 600 Мб даних і може зайняти до 30 хвилин залежно від швидкості мережі. Ці початкові дані завантажуються лише один раз і будуть доступні для всіх гаманців Litecoin", + "litecoin_what_is_mweb": "Що таке mweb?", "live_fee_rates": "Ставки плати за живий через API", "load_more": "Завантажити ще", "loading_your_wallet": "Завантаження гаманця", @@ -393,6 +401,8 @@ "monero_light_theme": "Легка тема Monero", "moonpay_alert_text": "Значення суми має бути більшим або дорівнювати ${minAmount} ${fiatCurrency}", "more_options": "Більше параметрів", + "mweb_confirmed": "Підтвердив Mweb", + "mweb_unconfirmed": "Неперевірений MWEB", "name": "Ім'я", "nano_current_rep": "Поточний представник", "nano_gpt_thanks_message": "Дякуємо за використання наногпта! Не забудьте повернутися до браузера після завершення транзакції!", @@ -815,6 +825,7 @@ "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": "Недостатньо доступних входів. Виберіть більше під контролем монети", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index bf128ca2a..da3d04a04 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "براہ کرم یقینی بنائیں کہ آپ اپنے لیجر پر صحیح ایپ کو کھولتے ہیں", "ledger_please_enable_bluetooth": "براہ کرم بلوٹوتھ کو اپنے لیجر کا پتہ لگانے کے لئے اہل بنائیں", "light_theme": "روشنی", + "litecoin_enable_mweb_sync": "MWEB اسکیننگ کو فعال کریں", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "MWEB ہمیشہ اسکیننگ سیٹ کریں", + "litecoin_mweb_display_card": "MWEB کارڈ دکھائیں", + "litecoin_mweb_scanning": "MWEB اسکیننگ", + "litecoin_mweb_settings": "MWEB کی ترتیبات", + "litecoin_mweb_warning": "MWEB کا استعمال ابتدائی طور پر m 600mb ڈیٹا ڈاؤن لوڈ کرے گا ، اور نیٹ ورک کی رفتار کے لحاظ سے 30 منٹ تک کا وقت لگ سکتا ہے۔ یہ ابتدائی اعداد و شمار صرف ایک بار ڈاؤن لوڈ کریں گے اور تمام لیٹیکوئن بٹوے کے لئے دستیاب ہوں گے", + "litecoin_what_is_mweb": "MWEB کیا ہے؟", "live_fee_rates": "API کے ذریعے براہ راست فیس کی شرح", "load_more": "مزید لوڈ کریں", "loading_your_wallet": "آپ کا بٹوہ لوڈ ہو رہا ہے۔", @@ -393,6 +401,8 @@ "monero_light_theme": "مونیرو لائٹ تھیم", "moonpay_alert_text": "رقم کی قدر ${minAmount} ${fiatCurrency} کے برابر یا زیادہ ہونی چاہیے۔", "more_options": "مزید زرائے", + "mweb_confirmed": "تصدیق شدہ MWEB", + "mweb_unconfirmed": "غیر مصدقہ MWEB", "name": "ﻡﺎﻧ", "nano_current_rep": "موجودہ نمائندہ", "nano_gpt_thanks_message": "نانوگپٹ استعمال کرنے کا شکریہ! اپنے لین دین کی تکمیل کے بعد براؤزر کی طرف واپس جانا یاد رکھیں!", @@ -816,6 +826,7 @@ "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": "کافی ان پٹ دستیاب نہیں ہے۔ براہ کرم سکے کے کنٹرول میں مزید منتخب کریں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index fc573451e..f4ed7aebb 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -394,6 +394,8 @@ "monero_light_theme": "Chủ đề sáng Monero", "moonpay_alert_text": "Giá trị số tiền phải lớn hơn hoặc bằng ${minAmount} ${fiatCurrency}", "more_options": "Thêm tùy chọn", + "mweb_confirmed": "Xác nhận MWEB", + "mweb_unconfirmed": "MWEB chưa được xác nhận", "name": "Tên", "nano_current_rep": "Đại diện hiện tại", "nano_gpt_thanks_message": "Cảm ơn bạn đã sử dụng NanoGPT! Hãy nhớ quay lại trình duyệt sau khi giao dịch của bạn hoàn tất!", @@ -815,6 +817,7 @@ "trusted": "Đã tin cậy", "tx_commit_exception_no_dust_on_change": "Giao dịch bị từ chối với số tiền này. Với số tiền này bạn có thể gửi ${min} mà không cần đổi tiền lẻ hoặc ${max} trả lại tiền lẻ.", "tx_commit_failed": "Giao dịch không thành công. Vui lòng liên hệ với hỗ trợ.", + "tx_commit_failed_no_peers": "Giao dịch không phát sóng, vui lòng thử lại trong một giây hoặc lâu hơn", "tx_invalid_input": "Bạn đang sử dụng loại đầu vào sai cho loại thanh toán này", "tx_no_dust_exception": "Giao dịch bị từ chối vì gửi một số tiền quá nhỏ. Vui lòng thử tăng số tiền.", "tx_not_enough_inputs_exception": "Không đủ đầu vào có sẵn. Vui lòng chọn thêm dưới Coin Control", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 1b8dfc065..8114d5ab1 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -364,6 +364,14 @@ "ledger_error_wrong_app": "Jọwọ rii daju pe iwọ yoo sọ app ti o tọ loju omi rẹ", "ledger_please_enable_bluetooth": "Jọwọ jẹ ki Bluetooth lati rii iṣupọ rẹ", "light_theme": "Funfun bí eérú", + "litecoin_enable_mweb_sync": "Mu mweb ọlọjẹ", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Ṣeto mweb nigbagbogbo n ṣayẹwo", + "litecoin_mweb_display_card": "Fihan kaadi Mweb", + "litecoin_mweb_scanning": "Mweb scanning", + "litecoin_mweb_settings": "Awọn eto Mweb", + "litecoin_mweb_warning": "Lilo Mweb yoo wa lakoko igbasilẹ ~ 600MB ti data, o le gba to iṣẹju 30 da lori iyara nẹtiwọọki. Awọn data akọkọ yii yoo ṣe igbasilẹ lẹẹkan si ki o wa fun gbogbo awọn Wolinkun LiveCooin", + "litecoin_what_is_mweb": "Kini mweb?", "live_fee_rates": "Awọn oṣuwọn Owo laaye laaye nipasẹ API", "load_more": "Ẹru diẹ sii", "loading_your_wallet": "A ń ṣí àpamọ́wọ́ yín", @@ -394,6 +402,8 @@ "monero_light_theme": "Monero Light Akori", "moonpay_alert_text": "Iye owó kò gbọ́dọ̀ kéré ju ${minAmount} ${fiatCurrency}", "more_options": "Ìyàn àfikún", + "mweb_confirmed": "Jẹrisi Mweb", + "mweb_unconfirmed": "Ajopo Mweb", "name": "Oruko", "nano_current_rep": "Aṣoju lọwọlọwọ", "nano_gpt_thanks_message": "O ṣeun fun lilo Nonnogt! Ranti lati tẹle pada si ẹrọ lilọ kiri ayelujara lẹhin iṣowo rẹ pari!", @@ -815,6 +825,7 @@ "trusted": "A ti fọkàn ẹ̀ tán", "tx_commit_exception_no_dust_on_change": "Iṣowo naa ti kọ pẹlu iye yii. Pẹlu awọn owó wọnyi o le firanṣẹ ${min} laisi ayipada tabi ${max} ni iyipada iyipada.", "tx_commit_failed": "Idunadura iṣowo kuna. Jọwọ kan si atilẹyin.", + "tx_commit_failed_no_peers": "Idunadura kuna lati wa igbohungbe, jọwọ gbiyanju lẹẹkansi ni iṣẹju keji tabi bẹẹ", "tx_invalid_input": "O nlo iru titẹ nkan ti ko tọ fun iru isanwo yii", "tx_no_dust_exception": "Iṣowo naa ni kọ nipa fifiranṣẹ iye ti o kere ju. Jọwọ gbiyanju pọ si iye naa.", "tx_not_enough_inputs_exception": "Ko to awọn titẹsi to. Jọwọ yan diẹ sii labẹ iṣakoso owo", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 9564b5982..de1bb4df4 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -363,6 +363,14 @@ "ledger_error_wrong_app": "请确保您在分类帐中操作正确的应用程序", "ledger_please_enable_bluetooth": "请启用蓝牙来检测您的分类帐", "light_theme": "艳丽", + "litecoin_enable_mweb_sync": "启用MWEB扫描", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "设置MWEB总是扫描", + "litecoin_mweb_display_card": "显示MWEB卡", + "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": "加载您的钱包", @@ -393,6 +401,8 @@ "monero_light_theme": "门罗币浅色主题", "moonpay_alert_text": "金额的价值必须大于或等于 ${minAmount} ${fiatCurrency}", "more_options": "更多选项", + "mweb_confirmed": "确认的MWEB", + "mweb_unconfirmed": "未经证实的MWEB", "name": "姓名", "nano_current_rep": "当前代表", "nano_gpt_thanks_message": "感谢您使用Nanogpt!事务完成后,请记住回到浏览器!", @@ -814,6 +824,7 @@ "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": "没有足够的输入。请在硬币控制下选择更多", diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index ec70f02a6..ad4ec984b 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -10,6 +10,7 @@ DIR=$(dirname "$0") case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh - $DIR/build_haven_all.sh ;; + $DIR/build_haven_all.sh + $DIR/build_mwebd.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/android/build_mwebd.sh b/scripts/android/build_mwebd.sh new file mode 100755 index 000000000..90dbc4c20 --- /dev/null +++ b/scripts/android/build_mwebd.sh @@ -0,0 +1,22 @@ +if [[ "$1" == "--dont-install" ]]; then + echo "Skipping Go installation as per --dont-install flag" +else + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init +fi + +# build mwebd: +git clone https://github.com/ltcmweb/mwebd +cd mwebd +git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935 +gomobile bind -target=android -androidapi 21 . +mkdir -p ../../../cw_mweb/android/libs/ +mv ./mwebd.aar $_ +# cleanup: +cd .. +rm -rf mwebd \ No newline at end of file diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index 565679e2d..ba5c55a1f 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -9,6 +9,6 @@ DIR=$(dirname "$0") case $APP_IOS_TYPE in "monero.com") $DIR/build_monero_all.sh ;; - "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh ;; + "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/ios/build_mwebd.sh b/scripts/ios/build_mwebd.sh new file mode 100755 index 000000000..f0fa64605 --- /dev/null +++ b/scripts/ios/build_mwebd.sh @@ -0,0 +1,20 @@ +#!/bin/bash +if [[ "$1" == "--dont-install" ]]; then + echo "Skipping Go installation as per --dont-install flag" +else + # install go > 1.23: + brew install go + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init +fi + +# build mwebd: +git clone https://github.com/ltcmweb/mwebd +cd mwebd +git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935 +gomobile bind -target=ios . +mv -fn ./Mwebd.xcframework ../../../ios/ +# cleanup: +cd .. +rm -rf mwebd \ No newline at end of file diff --git a/tool/configure.dart b/tool/configure.dart index 815c9e399..362a6d5d6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -115,6 +115,7 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart'; @@ -180,8 +181,8 @@ abstract class Bitcoin { List getUnspents(Object wallet); Future updateUnspents(Object wallet); WalletService createBitcoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); - WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource, bool isDirect); + Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); + WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); TransactionPriority getBitcoinTransactionPriorityMedium(); TransactionPriority getBitcoinTransactionPriorityCustom(); TransactionPriority getLitecoinTransactionPriorityMedium(); @@ -195,6 +196,7 @@ abstract class Bitcoin { Future setAddressType(Object wallet, dynamic option); ReceivePageOption getSelectedAddressType(Object wallet); List getBitcoinReceivePageOptions(); + List getLitecoinReceivePageOptions(); BitcoinAddressType getBitcoinAddressType(ReceivePageOption option); bool hasSelectedSilentPayments(Object wallet); bool isBitcoinReceivePageOption(ReceivePageOption option); @@ -214,16 +216,19 @@ abstract class Bitcoin { int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, {int? size}); Future checkIfMempoolAPIIsEnabled(Object wallet); Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}); + int getLitecoinHeightByDate({required DateTime date}); Future rescan(Object wallet, {required int height, bool? doSingleScan}); Future getNodeIsElectrsSPEnabled(Object wallet); void deleteSilentPaymentAddress(Object wallet, String address); Future updateFeeRates(Object wallet); int getMaxCustomFeeRate(Object wallet); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); List updateOutputs(PendingTransaction pendingTransaction, List outputs); bool txIsReceivedSilentPayment(TransactionInfo txInfo); + bool txIsMweb(TransactionInfo txInfo); + Future setMwebEnabled(Object wallet, bool enabled); + bool getMwebEnabled(Object wallet); } """; From f8b0c0ad2af05bc5c53e61e7c23522b20322ffd4 Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 28 Sep 2024 04:38:23 +0200 Subject: [PATCH 106/142] CW-611-Refactor-Address-Handling (#1630) * subaddress fix * fix subaddress generation * rewrite usedAddresses for xmr and wow * [skip ci] remove print statements * refactor address handling * do not remove manual addresses, just mark them * monero display latest address on receive page when autogenerate is enabled [skip ci] * WIP subaddresses, hidden addresses, and UI improvements for monero * update configure script * fix subaddress generation, display latest address * Update lib/core/wallet_loading_service.dart Co-authored-by: Omar Hatem * Exclude manually created addresses Co-authored-by: Omar Hatem * don't call .save function multiple times Co-authored-by: Omar Hatem * - revert usedAddress functinality - add mutex to prevent crashes - fix UI flashing in tx screen - fixes from comments * account index fixes added code to wownero code comment * - added subaddress index - fixed received count also accounting for change (we don't want that) - fix bad state: no element - fix search - fix automatic generation * prevent crashes by acquiring mutex before setting the pointer * - fix ttDetails generation in larger/restored wallets - show manual add icon in monero/wownero even when autogeneration is enabled - disable colors on non-debug builds - cache getAddress call in xmr/wow [skip ci] * fix: silent payment error in address setter enable fancy new features only for xmr / wow * refresh subaddress list, when we add new address fix manual addresses marking * add toggle to hide and show address * update transaction details after restore * show only one address in address book for xmr, wow and haven * fix address book reset address only when autogenerate is on * enable isEnabledAutoGenerateSubaddress on new wallets * hide addresses after exchange only for XMR and WOW * fix: bad-state no element * Update cw_monero/lib/monero_wallet_addresses.dart Co-authored-by: Omar Hatem * Update cw_monero/lib/monero_wallet_addresses.dart Co-authored-by: Omar Hatem * improvements to performance * 0, 0 -> accountIndex, addressIndex * make constant variables final * Update cw_wownero/lib/wownero_wallet_addresses.dart [skip ci] * Update cw_wownero/lib/wownero_wallet_addresses.dart [skip ci] * Update cw_monero/lib/monero_wallet.dart [skip ci] * fix potential exception * fix after removing late * remove orElse, replaced it with a try catch block. fix strings * fix valid seed function * fix null check error [skip ci] * fix updateSubaddressList for wow and haven --------- Co-authored-by: Omar Hatem --- cw_bitcoin/lib/electrum_wallet.dart | 3 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 12 +- cw_core/lib/subaddress.dart | 14 +- cw_core/lib/wallet_addresses.dart | 40 +++- cw_core/lib/wallet_info.dart | 9 + cw_haven/lib/haven_wallet_addresses.dart | 8 +- cw_monero/lib/api/subaddress_list.dart | 82 ++++++- cw_monero/lib/api/transaction_history.dart | 51 +++-- cw_monero/lib/api/wallet.dart | 15 +- cw_monero/lib/monero_subaddress_list.dart | 24 +- cw_monero/lib/monero_wallet.dart | 19 +- cw_monero/lib/monero_wallet_addresses.dart | 40 +++- cw_wownero/lib/api/subaddress_list.dart | 65 +++++- cw_wownero/lib/api/transaction_history.dart | 37 +++- cw_wownero/lib/api/wallet.dart | 13 +- cw_wownero/lib/wownero_subaddress_list.dart | 8 + cw_wownero/lib/wownero_wallet.dart | 10 +- cw_wownero/lib/wownero_wallet_addresses.dart | 35 ++- lib/monero/cw_monero.dart | 17 +- .../screens/dashboard/pages/address_page.dart | 5 +- lib/src/screens/exchange/exchange_page.dart | 12 +- .../exchange/exchange_template_page.dart | 4 +- lib/src/screens/receive/receive_page.dart | 3 +- .../screens/receive/widgets/address_cell.dart | 41 +++- .../screens/receive/widgets/address_list.dart | 209 ++++++++++++------ .../screens/receive/widgets/header_tile.dart | 7 +- .../screens/receive/widgets/qr_widget.dart | 14 +- .../contact_list/contact_list_view_model.dart | 51 +++-- .../exchange/exchange_view_model.dart | 15 +- ...let_address_edit_or_create_view_model.dart | 4 + .../wallet_address_hidden_list_header.dart | 3 + .../wallet_address_list_item.dart | 6 +- .../wallet_address_list_view_model.dart | 132 +++++++---- res/values/strings_en.arb | 3 + tool/configure.dart | 6 +- 35 files changed, 775 insertions(+), 242 deletions(-) create mode 100644 lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 17510e0de..4f8f3eab5 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1772,7 +1772,8 @@ abstract class ElectrumWalletBase final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true); final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false); - + walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); + await walletAddresses.saveAddressesInBox(); await Future.wait(addressesByType.map((addressRecord) async { final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip()); diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 2de86d780..61d4fdbc3 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -163,6 +163,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override set address(String addr) { + if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) { + return; + } if (addressPageType == SilentPaymentsAddresType.p2sp) { final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); @@ -174,12 +177,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } return; } - - final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); + try { + final addressRecord = _addresses.firstWhere( + (addressRecord) => addressRecord.address == addr, + ); previousAddressRecord = addressRecord; receiveAddresses.remove(addressRecord); receiveAddresses.insert(0, addressRecord); + } catch (e) { + print("ElectrumWalletAddressBase: set address ($addr): $e"); + } } @override diff --git a/cw_core/lib/subaddress.dart b/cw_core/lib/subaddress.dart index 8571544a9..4dba7c836 100644 --- a/cw_core/lib/subaddress.dart +++ b/cw_core/lib/subaddress.dart @@ -1,12 +1,22 @@ class Subaddress { - Subaddress({required this.id, required this.address, required this.label}); + Subaddress({ + required this.id, + required this.address, + required this.label, + this.balance = null, + this.txCount = null, + }); Subaddress.fromMap(Map map) : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String), this.address = (map['address'] ?? '') as String, - this.label = (map['label'] ?? '') as String; + this.label = (map['label'] ?? '') as String, + this.balance = (map['balance'] ?? '') as String?, + this.txCount = (map['txCount'] ?? '') as int?; final int id; final String address; final String label; + final String? balance; + final int? txCount; } diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index e987b5d0e..ca488cfed 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,26 +1,58 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; abstract class WalletAddresses { WalletAddresses(this.walletInfo) : addressesMap = {}, allAddressesMap = {}, - addressInfos = {}; + addressInfos = {}, + usedAddresses = {}, + hiddenAddresses = walletInfo.hiddenAddresses?.toSet() ?? {}, + manualAddresses = walletInfo.manualAddresses?.toSet() ?? {}; final WalletInfo walletInfo; String get address; + String get latestAddress { + if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) { + if (addressesMap.keys.length == 0) return address; + return addressesMap[addressesMap.keys.last] ?? address; + } + return _localAddress ?? address; + } + String? get primaryAddress => null; - set address(String address); + String? _localAddress; + + set address(String address) => _localAddress = address; + + String get addressForExchange => address; Map addressesMap; Map allAddressesMap; + Map get usableAddressesMap { + final tmp = addressesMap.map((key, value) => MapEntry(key, value)); // copy address map + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key)); + return tmp; + } + + Map get usableAllAddressesMap { + final tmp = allAddressesMap.map((key, value) => MapEntry(key, value)); // copy address map + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key)); + return tmp; + } + Map> addressInfos; - Set usedAddresses = {}; + Set usedAddresses; + + Set hiddenAddresses; + + Set manualAddresses; Future init(); @@ -32,6 +64,8 @@ abstract class WalletAddresses { walletInfo.addresses = addressesMap; walletInfo.addressInfos = addressInfos; walletInfo.usedAddresses = usedAddresses.toList(); + walletInfo.hiddenAddresses = hiddenAddresses.toList(); + walletInfo.manualAddresses = manualAddresses.toList(); if (walletInfo.isInBox) { await walletInfo.save(); diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 0b0e8e4c5..bd035e30a 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -189,6 +189,15 @@ class WalletInfo extends HiveObject { @HiveField(22) String? parentAddress; + + @HiveField(23) + List? hiddenAddresses; + + @HiveField(24) + List? manualAddresses; + + + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart index eeeb763cf..06de44dff 100644 --- a/cw_haven/lib/haven_wallet_addresses.dart +++ b/cw_haven/lib/haven_wallet_addresses.dart @@ -1,6 +1,7 @@ import 'package:cw_core/wallet_addresses_with_account.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/account.dart'; +import 'package:cw_haven/api/wallet.dart'; import 'package:cw_haven/haven_account_list.dart'; import 'package:cw_haven/haven_subaddress_list.dart'; import 'package:cw_core/subaddress.dart'; @@ -36,7 +37,7 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -81,8 +82,9 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount monero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, - ); + late String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); final int addressIndex; final int accountIndex; - String get label => monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + final int received; + final int txCount; + String get label { + final localLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + if (localLabel.startsWith("#$addressIndex")) return localLabel; // don't duplicate the ID if it was user-providen + return "#$addressIndex ${localLabel}".trim(); + } } +class TinyTransactionDetails { + TinyTransactionDetails({ + required this.address, + required this.amount, + }); + final List address; + final int amount; +} + +int lastWptr = 0; +int lastTxCount = 0; +List ttDetails = []; + List getAllSubaddresses() { + txhistory = monero.Wallet_history(wptr!); + final txCount = monero.TransactionHistory_count(txhistory!); + if (lastTxCount != txCount && lastWptr != wptr!.address) { + final List newttDetails = []; + lastTxCount = txCount; + lastWptr = wptr!.address; + 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); + newttDetails.add(TinyTransactionDetails( + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), + amount: monero.TransactionInfo_amount(tx), + )); + } + ttDetails.clear(); + ttDetails.addAll(newttDetails); + } final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final list = List.generate(size, (index) { + final ttDetailsLocal = ttDetails.where((element) { + final address = getAddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + if (element.address.contains(address)) return true; + return false; + }).toList(); + int received = 0; + for (var i = 0; i < ttDetailsLocal.length; i++) { + received += ttDetailsLocal[i].amount; + } return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, + received: received, + txCount: ttDetailsLocal.length, ); }).reversed.toList(); if (list.length == 0) { - list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0)); + list.add( + Subaddress( + addressIndex: subaddress!.accountIndex, + accountIndex: 0, + received: 0, + txCount: 0, + )); } return list; } +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); diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index bd8b46356..a308b682e 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -5,32 +5,42 @@ import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/wallet.dart'; import 'package:ffi/ffi.dart'; import 'package:monero/monero.dart' as monero; import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen; +import 'package:mutex/mutex.dart'; String getTxKey(String txId) { return monero.Wallet_getTxKey(wptr!, txid: txId); } - +final txHistoryMutex = Mutex(); monero.TransactionHistory? txhistory; - -void refreshTransactions() { +bool isRefreshingTx = false; +Future refreshTransactions() async { + if (isRefreshingTx == true) return; + isRefreshingTx = true; txhistory ??= monero.Wallet_history(wptr!); - monero.TransactionHistory_refresh(txhistory!); + final ptr = txhistory!.address; + await txHistoryMutex.acquire(); + await Isolate.run(() { + monero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); + }); + txHistoryMutex.release(); + isRefreshingTx = false; } int countOfTransactions() => monero.TransactionHistory_count(txhistory!); -List getAllTransactions() { +Future> getAllTransactions() async { List dummyTxs = []; - + + await txHistoryMutex.acquire(); txhistory ??= monero.Wallet_history(wptr!); - monero.TransactionHistory_refresh(txhistory!); int size = countOfTransactions(); final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index))); - + txHistoryMutex.release(); final accts = monero.Wallet_numSubaddressAccounts(wptr!); for (var i = 0; i < accts; i++) { final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i); @@ -45,6 +55,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -251,19 +263,28 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = monero.Wallet_address( + late final String subaddressLabel = monero.Wallet_getSubaddressLabel( wptr!, - accountIndex: 0, - addressIndex: 0, + accountIndex: accountIndex, + addressIndex: addressIndex, ); + late final String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); + late final List addressList = List.generate(addressIndexList.length, (index) => + getAddress( + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 10; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; @@ -309,6 +330,8 @@ class Transaction { 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), @@ -331,6 +354,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndexList, + required this.addressIndex, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index b493e536e..8e03cff3e 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -66,9 +66,20 @@ String getSeedLegacy(String? language) { return legacy; } -String getAddress({int accountIndex = 0, int addressIndex = 0}) => - monero.Wallet_address(wptr!, +Map>> addressCache = {}; + +String getAddress({int accountIndex = 0, int addressIndex = 0}) { + // print("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}"); + while (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + print("adding subaddress"); + monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + } + addressCache[wptr!.address] ??= {}; + addressCache[wptr!.address]![accountIndex] ??= {}; + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + return addressCache[wptr!.address]![accountIndex]![addressIndex]!; +} int getFullBalance({int accountIndex = 0}) => monero.Wallet_balance(wptr!, accountIndex: accountIndex); diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index c35afb282..c20b23b5e 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_monero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -54,18 +55,12 @@ abstract class MoneroSubaddressListBase with Store { final address = s.address; final label = s.label; final id = s.addressIndex; - final hasDefaultAddressName = - label.toLowerCase() == 'Primary account'.toLowerCase() || - label.toLowerCase() == 'Untitled account'.toLowerCase(); - final isPrimaryAddress = id == 0 && hasDefaultAddressName; return Subaddress( id: id, address: address, - label: isPrimaryAddress - ? 'Primary address' - : hasDefaultAddressName - ? '' - : label); + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, + label: label); }).toList(); } @@ -103,6 +98,9 @@ abstract class MoneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -124,7 +122,8 @@ abstract class MoneroSubaddressListBase with Store { Future> _getAllUnusedAddresses( {required int accountIndex, required String label}) async { final allAddresses = subaddress_list.getAllSubaddresses(); - if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last)) { + // first because addresses come in reversed order. + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.first.address)) { final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); if (!isAddressUnused) { return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); @@ -139,12 +138,13 @@ abstract class MoneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' : label); - }) - .toList(); + }).toList().reversed.toList(); } Future _newSubaddress({required int accountIndex, required String label}) async { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 8773d694d..c6d5d2e5f 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -59,7 +59,7 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -128,6 +131,7 @@ abstract class MoneroWalletBase extends WalletBase await save()); + // update transaction details after restore + walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0); } @override @@ -167,6 +173,7 @@ abstract class MoneroWalletBase extends WalletBase> fetchTransactions() async { transaction_history.refreshTransactions(); - return _getAllTransactionsOfAccount(walletAddresses.account?.id) + return (await _getAllTransactionsOfAccount(walletAddresses.account?.id)) .fold>( {}, (Map acc, MoneroTransactionInfo tx) { @@ -594,8 +601,8 @@ abstract class MoneroWalletBase extends WalletBase monero_wallet.getSubaddressLabel(accountIndex, addressIndex); - List _getAllTransactionsOfAccount(int? accountIndex) => - transaction_history - .getAllTransactions() + Future> _getAllTransactionsOfAccount(int? accountIndex) async => + (await transaction_history + .getAllTransactions()) .map( (row) => MoneroTransactionInfo( row.hash, diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index d4f22e46f..c8a4448a4 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_account_list.dart'; import 'package:cw_monero/monero_subaddress_list.dart'; @@ -27,6 +29,30 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get latestAddress { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + subaddressList.update(accountIndex: account?.id??0); + } + return address; + } + + @override + String get addressForExchange { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address) || manualAddresses.contains(address) || subaddress_list.getRawLabel(accountIndex: account?.id??0, addressIndex: addressIndex).isNotEmpty) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + subaddressList.update(accountIndex: account?.id??0); + } + return address; + } + @observable Account? account; @@ -37,10 +63,12 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { MoneroAccountList accountList; + Set usedAddresses = Set(); + @override Future init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -89,8 +117,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { void updateSubaddressList({required int accountIndex}) { subaddressList.update(accountIndex: accountIndex); - subaddress = subaddressList.subaddresses.first; - address = subaddress!.address; + address = subaddressList.subaddresses.isNotEmpty + ? subaddressList.subaddresses.first.address + : getAddress(); } Future updateUsedSubaddress() async { @@ -109,7 +138,10 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last; + if (num.tryParse(subaddress!.balance??'0') != 0) { + getAddress(accountIndex: accountIndex, addressIndex: (subaddress?.id??0)+1); + } address = subaddress!.address; } diff --git a/cw_wownero/lib/api/subaddress_list.dart b/cw_wownero/lib/api/subaddress_list.dart index d8c91a584..b07f24c4a 100644 --- a/cw_wownero/lib/api/subaddress_list.dart +++ b/cw_wownero/lib/api/subaddress_list.dart @@ -1,4 +1,5 @@ import 'package:cw_wownero/api/account_list.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/wallet.dart'; import 'package:monero/wownero.dart' as wownero; @@ -28,27 +29,75 @@ class Subaddress { Subaddress({ required this.addressIndex, required this.accountIndex, + required this.txCount, + required this.received, }); - String get address => wownero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, - ); + late String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); final int addressIndex; final int accountIndex; String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + final int txCount; + final int received; } +class TinyTransactionDetails { + TinyTransactionDetails({ + required this.address, + required this.amount, + }); + final List address; + final int amount; +} + +int lastWptr = 0; +int lastTxCount = 0; +List ttDetails = []; + List getAllSubaddresses() { + txhistory = wownero.Wallet_history(wptr!); + final txCount = wownero.TransactionHistory_count(txhistory!); + if (lastTxCount != txCount && lastWptr != wptr!.address) { + final List newttDetails = []; + lastTxCount = txCount; + lastWptr = wptr!.address; + for (var i = 0; i < txCount; i++) { + final tx = wownero.TransactionHistory_transaction(txhistory!, index: i); + final subaddrs = wownero.TransactionInfo_subaddrIndex(tx).split(","); + final account = wownero.TransactionInfo_subaddrAccount(tx); + newttDetails.add(TinyTransactionDetails( + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), + amount: wownero.TransactionInfo_amount(tx), + )); + } + ttDetails.clear(); + ttDetails.addAll(newttDetails); + } final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final list = List.generate(size, (index) { + final ttDetailsLocal = ttDetails.where((element) { + final address = getAddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + if (address == element.address) return true; + return false; + }).toList(); + int received = 0; + for (var i = 0; i < ttDetailsLocal.length; i++) { + received += ttDetailsLocal[i].amount; + } return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, + received: received, + txCount: ttDetailsLocal.length, ); }).reversed.toList(); if (list.isEmpty) { - list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex)); + list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0)); } return list; } @@ -58,6 +107,10 @@ void addSubaddressSync({required int accountIndex, required String label}) { refreshSubaddresses(accountIndex: accountIndex); } +int numSubaddresses(int subaccountIndex) { + return wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex); +} + void setLabelForSubaddressSync( {required int accountIndex, required int addressIndex, required String label}) { wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label); diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart index a1e1e3c9b..6b0923e83 100644 --- a/cw_wownero/lib/api/transaction_history.dart +++ b/cw_wownero/lib/api/transaction_history.dart @@ -3,6 +3,7 @@ import 'dart:isolate'; import 'package:cw_wownero/api/account_list.dart'; import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart'; +import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/api/wownero_output.dart'; import 'package:cw_wownero/api/structs/pending_transaction.dart'; import 'package:ffi/ffi.dart'; @@ -16,9 +17,16 @@ String getTxKey(String txId) { wownero.TransactionHistory? txhistory; -void refreshTransactions() { +bool isRefreshingTx = false; +Future refreshTransactions() async { + if (isRefreshingTx == true) return; + isRefreshingTx = true; txhistory ??= wownero.Wallet_history(wptr!); - wownero.TransactionHistory_refresh(txhistory!); + final ptr = txhistory!.address; + await Isolate.run(() { + wownero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); + }); + isRefreshingTx = false; } int countOfTransactions() => wownero.TransactionHistory_count(txhistory!); @@ -45,6 +53,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -243,23 +253,28 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = wownero.Wallet_address( - wptr!, - accountIndex: 0, - addressIndex: 0, + late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + late final String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, ); + late final List addressList = List.generate(addressIndexList.length, (index) => + getAddress( + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 3; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; - late DateTime timeStamp; + late final DateTime timeStamp; late final bool isConfirmed = !isPending; final String hash; final String key; @@ -301,6 +316,8 @@ class Transaction { amount = wownero.TransactionInfo_amount(txInfo), paymentId = wownero.TransactionInfo_paymentId(txInfo), accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo), + addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, + addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), blockheight = wownero.TransactionInfo_blockHeight(txInfo), confirmations = wownero.TransactionInfo_confirmations(txInfo), fee = wownero.TransactionInfo_fee(txInfo), @@ -314,6 +331,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndex, + required this.addressIndexList, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 56f54dfac..baf9c8960 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -67,10 +67,19 @@ String getSeedLegacy(String? language) { } return legacy; } +Map>> addressCache = {}; -String getAddress({int accountIndex = 0, int addressIndex = 1}) => - wownero.Wallet_address(wptr!, +String getAddress({int accountIndex = 0, int addressIndex = 1}) { + while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + print("adding subaddress"); + wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + } + addressCache[wptr!.address] ??= {}; + addressCache[wptr!.address]![accountIndex] ??= {}; + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= wownero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + return addressCache[wptr!.address]![accountIndex]![addressIndex]!; +} int getFullBalance({int accountIndex = 0}) => wownero.Wallet_balance(wptr!, accountIndex: accountIndex); diff --git a/cw_wownero/lib/wownero_subaddress_list.dart b/cw_wownero/lib/wownero_subaddress_list.dart index 61fd09ef9..2ef4f2045 100644 --- a/cw_wownero/lib/wownero_subaddress_list.dart +++ b/cw_wownero/lib/wownero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_wownero/api/coins_info.dart'; import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_wownero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -61,6 +62,8 @@ abstract class WowneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: isPrimaryAddress ? 'Primary address' : hasDefaultAddressName @@ -103,6 +106,9 @@ abstract class WowneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -140,6 +146,8 @@ abstract class WowneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index c3f4bcb69..02d968eb3 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -59,7 +59,7 @@ abstract class WowneroWalletBase _isTransactionUpdating = false, _hasSyncAfterStartup = false, _password = password, - isEnabledAutoGenerateSubaddress = false, + isEnabledAutoGenerateSubaddress = true, syncStatus = NotConnectedSyncStatus(), unspentCoins = [], this.unspentCoinsInfo = unspentCoinsInfo, @@ -82,6 +82,10 @@ abstract class WowneroWalletBase reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -123,6 +127,7 @@ abstract class WowneroWalletBase wownero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; + ReactionDisposer? _onTxHistoryChangeReaction; bool _isTransactionUpdating; bool _hasSyncAfterStartup; Timer? _autoSaveTimer; @@ -158,6 +163,7 @@ abstract class WowneroWalletBase void close() async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); + _onTxHistoryChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); } @@ -564,8 +570,8 @@ abstract class WowneroWalletBase } _isTransactionUpdating = true; - transactionHistory.clear(); final transactions = await fetchTransactions(); + transactionHistory.clear(); transactionHistory.addMany(transactions); await transactionHistory.save(); _isTransactionUpdating = false; diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index 9eeb182eb..b2f9ec67a 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; +import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/wownero_account_list.dart'; import 'package:cw_wownero/wownero_subaddress_list.dart'; @@ -27,6 +29,27 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get latestAddress { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + } + return address; + } + + @override + String get addressForExchange { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address) || manualAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + } + return address; + } @observable Account? account; @@ -36,11 +59,14 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; + + @override + Set usedAddresses = Set(); @override Future init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -89,8 +115,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { void updateSubaddressList({required int accountIndex}) { subaddressList.update(accountIndex: accountIndex); - subaddress = subaddressList.subaddresses.first; - address = subaddress!.address; + address = subaddressList.subaddresses.isNotEmpty + ? subaddressList.subaddresses.first.address + : getAddress(); } Future updateUsedSubaddress() async { @@ -109,7 +136,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last; address = subaddress!.address; } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 1f1888b44..b1cf49482 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -61,7 +61,13 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { ObservableList get subaddresses { final moneroWallet = _wallet as MoneroWallet; final subAddresses = moneroWallet.walletAddresses.subaddressList.subaddresses - .map((sub) => Subaddress(id: sub.id, address: sub.address, label: sub.label)) + .map((sub) => Subaddress( + id: sub.id, + address: sub.address, + label: sub.label, + received: sub.balance??"unknown", + txCount: sub.txCount??0, + )) .toList(); return ObservableList.of(subAddresses); } @@ -83,7 +89,12 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { final moneroWallet = wallet as MoneroWallet; return moneroWallet.walletAddresses.subaddressList .getAll() - .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address)) + .map((sub) => Subaddress( + id: sub.id, + label: sub.label, + address: sub.address, + txCount: sub.txCount??0, + received: sub.balance??'unknown')) .toList(); } @@ -91,7 +102,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { Future addSubaddress(Object wallet, {required int accountIndex, required String label}) async { final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.subaddressList + return await moneroWallet.walletAddresses.subaddressList .addSubaddress(accountIndex: accountIndex, label: label); } diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 2040cbf9d..10f9aef43 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -155,13 +155,14 @@ class AddressPage extends BasePage { amountTextFieldFocusNode: _cryptoAmountFocus, amountController: _amountController, isLight: dashboardViewModel.settingsStore.currentTheme.type == - ThemeType.light))), + ThemeType.light, + ))), SizedBox(height: 16), Observer(builder: (_) { if (addressListViewModel.hasAddressList) { return SelectButton( text: addressListViewModel.buttonTitle, - onTap: () async => Navigator.of(context).pushNamed(Routes.receive), + onTap: () => Navigator.of(context).pushNamed(Routes.receive), textColor: Theme.of(context).extension()!.textColor, color: Theme.of(context).extension()!.syncedBackgroundColor, borderColor: Theme.of(context).extension()!.cardBorderColor, diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 78b4d0db8..2f8e3eb5c 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -509,7 +509,7 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) { + reaction((_) => exchangeViewModel.wallet.walletAddresses.addressForExchange, (String address) { if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) { depositKey.currentState!.changeAddress(address: address); } @@ -565,7 +565,7 @@ class ExchangePage extends BasePage { key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : ''); key.currentState!.changeAddress( - address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : ''); + address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : ''); key.currentState!.changeAmount(amount: ''); } @@ -576,9 +576,9 @@ class ExchangePage extends BasePage { if (isCurrentTypeWallet) { key.currentState!.changeWalletName(exchangeViewModel.wallet.name); - key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address; + key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.addressForExchange; } else if (key.currentState!.addressController.text == - exchangeViewModel.wallet.walletAddresses.address) { + exchangeViewModel.wallet.walletAddresses.addressForExchange) { key.currentState!.changeWalletName(''); key.currentState!.addressController.text = ''; } @@ -629,7 +629,7 @@ class ExchangePage extends BasePage { initialCurrency: exchangeViewModel.depositCurrency, initialWalletName: depositWalletName ?? '', initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.depositAddress, initialIsAmountEditable: true, initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, @@ -694,7 +694,7 @@ class ExchangePage extends BasePage { initialCurrency: exchangeViewModel.receiveCurrency, initialWalletName: receiveWalletName ?? '', initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.receiveAddress, initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable, isAmountEstimated: true, diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index 4edc9095a..f82318ca2 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -129,7 +129,7 @@ class ExchangeTemplatePage extends BasePage { initialWalletName: depositWalletName ?? '', initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.depositAddress, initialIsAmountEditable: true, initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, @@ -166,7 +166,7 @@ class ExchangeTemplatePage extends BasePage { initialWalletName: receiveWalletName ?? '', initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.receiveAddress, initialIsAmountEditable: false, isAmountEstimated: true, diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 724e5c3bd..7e3c2b555 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -121,7 +121,8 @@ class ReceivePage extends BasePage { heroTag: _heroTag, amountTextFieldFocusNode: _cryptoAmountFocus, amountController: _amountController, - isLight: currentTheme.type == ThemeType.light), + isLight: currentTheme.type == ThemeType.light, + ), ), AddressList(addressListViewModel: addressListViewModel), Padding( diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 850c08209..beef7c762 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -2,6 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -15,11 +16,14 @@ class AddressCell extends StatelessWidget { required this.textColor, this.onTap, this.onEdit, + this.onHide, + this.isHidden = false, this.onDelete, this.txCount, this.balance, this.isChange = false, - this.hasBalance = false}); + this.hasBalance = false, + this.hasReceived = false}); factory AddressCell.fromItem( WalletAddressListItem item, { @@ -28,7 +32,10 @@ class AddressCell extends StatelessWidget { required Color textColor, Function(String)? onTap, bool hasBalance = false, + bool hasReceived = false, Function()? onEdit, + Function()? onHide, + bool isHidden = false, Function()? onDelete, }) => AddressCell( @@ -40,11 +47,14 @@ class AddressCell extends StatelessWidget { textColor: textColor, onTap: onTap, onEdit: onEdit, + onHide: onHide, + isHidden: isHidden, onDelete: onDelete, txCount: item.txCount, balance: item.balance, isChange: item.isChange, - hasBalance: hasBalance); + hasBalance: hasBalance, + hasReceived: hasReceived,); final String address; final String name; @@ -54,11 +64,14 @@ class AddressCell extends StatelessWidget { final Color textColor; final Function(String)? onTap; final Function()? onEdit; + final Function()? onHide; + final bool isHidden; final Function()? onDelete; final int? txCount; final String? balance; final bool isChange; final bool hasBalance; + final bool hasReceived; static const int addressPreviewLength = 8; @@ -138,7 +151,7 @@ class AddressCell extends StatelessWidget { ), ], ), - if (hasBalance) + if (hasBalance || hasReceived) Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( @@ -146,7 +159,7 @@ class AddressCell extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Text( - '${S.of(context).balance}: $balance', + '${hasReceived ? S.of(context).received : S.of(context).balance}: $balance', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -178,14 +191,28 @@ class AddressCell extends StatelessWidget { enabled: !isCurrent, child: Slidable( key: Key(address), - startActionPane: _actionPane(context), - endActionPane: _actionPane(context), + startActionPane: _actionPaneStart(context), + endActionPane: _actionPaneEnd(context), child: cell, ), ); } - ActionPane _actionPane(BuildContext context) => ActionPane( + ActionPane _actionPaneEnd(BuildContext context) => ActionPane( + motion: const ScrollMotion(), + extentRatio: onDelete != null ? 0.4 : 0.3, + children: [ + SlidableAction( + onPressed: (_) => onHide?.call(), + backgroundColor: isHidden ? Colors.green : Colors.red, + foregroundColor: Colors.white, + icon: isHidden ? CupertinoIcons.arrow_left : CupertinoIcons.arrow_right, + label: isHidden ? S.of(context).show : S.of(context).hide, + ), + ], + ); + + ActionPane _actionPaneStart(BuildContext context) => ActionPane( motion: const ScrollMotion(), extentRatio: onDelete != null ? 0.4 : 0.3, children: [ diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 27ec8c33a..9f15018d0 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -10,16 +10,19 @@ import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart'; import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -class AddressList extends StatelessWidget { +class AddressList extends StatefulWidget { const AddressList({ super.key, required this.addressListViewModel, @@ -29,59 +32,123 @@ class AddressList extends StatelessWidget { final WalletAddressListViewModel addressListViewModel; final Function(String)? onSelect; + @override + State createState() => _AddressListState(); +} + +class _AddressListState extends State { + + bool showHiddenAddresses = false; + + void _toggleHiddenAddresses() { + setState(() { + showHiddenAddresses = !showHiddenAddresses; + }); + updateItems(); + } + + List getItems(List list, bool showHidden) { + return list.where((element) { + if (element is WalletAddressListItem) { + if (showHidden && element.isHidden) return true; + if (!showHidden && !element.isHidden) return true; + return false; + } + return true; + }).toList(); + } + + List items = []; + + void updateItems() { + setState(() { + items = getItems(widget.addressListViewModel.items, showHiddenAddresses); + }); + } + + @override + void initState() { + super.initState(); + + items = getItems(widget.addressListViewModel.items, showHiddenAddresses); + } + @override Widget build(BuildContext context) { - bool editable = onSelect == null; - return Observer( - builder: (_) => ListView.separated( - padding: EdgeInsets.all(0), - separatorBuilder: (context, _) => const HorizontalSectionDivider(), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: addressListViewModel.items.length, - itemBuilder: (context, index) { - final item = addressListViewModel.items[index]; - Widget cell = Container(); + bool editable = widget.onSelect == null; + return ListView.separated( + padding: EdgeInsets.all(0), + separatorBuilder: (context, _) => const HorizontalSectionDivider(), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + Widget cell = Container(); - if (item is WalletAccountListHeader) { - cell = HeaderTile( - showTrailingButton: true, - walletAddressListViewModel: addressListViewModel, - trailingButtonTap: () async { - if (addressListViewModel.type == WalletType.monero || - addressListViewModel.type == WalletType.haven) { - await showPopUp( - context: context, builder: (_) => getIt.get()); - } else { - await showPopUp( - context: context, builder: (_) => getIt.get()); - } - }, - title: S.of(context).accounts, - trailingIcon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).extension()!.iconsColor, - )); - } + if (item is WalletAccountListHeader) { + cell = HeaderTile( + showTrailingButton: true, + walletAddressListViewModel: widget.addressListViewModel, + trailingButtonTap: () async { + if (widget.addressListViewModel.type == WalletType.monero || + widget.addressListViewModel.type == WalletType.haven) { + await showPopUp( + context: context, builder: (_) => getIt.get()); + updateItems(); + } else { + await showPopUp( + context: context, builder: (_) => getIt.get()); + updateItems(); + } + }, + title: S.of(context).accounts, + trailingIcon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + )); + } - if (item is WalletAddressListHeader) { - cell = HeaderTile( - title: S.of(context).addresses, - walletAddressListViewModel: addressListViewModel, - showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled, - showSearchButton: true, - trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress), - trailingIcon: Icon( - Icons.add, - size: 20, - color: Theme.of(context).extension()!.iconsColor, - )); - } + if (item is WalletAddressHiddenListHeader) { + cell = HeaderTile( + title: S.of(context).hidden_addresses, + walletAddressListViewModel: widget.addressListViewModel, + showTrailingButton: true, + showSearchButton: false, + trailingButtonTap: _toggleHiddenAddresses, + trailingIcon: Icon( + showHiddenAddresses ? Icons.toggle_on : Icons.toggle_off, + size: 20, + color: Theme.of(context).extension()!.iconsColor, + )); + } - if (item is WalletAddressListItem) { + if (item is WalletAddressListHeader) { + cell = HeaderTile( + title: S.of(context).addresses, + walletAddressListViewModel: widget.addressListViewModel, + showTrailingButton: widget.addressListViewModel.showAddManualAddresses, + showSearchButton: true, + onSearchCallback: updateItems, + trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { + updateItems(); // refresh the new address + }), + trailingIcon: Icon( + Icons.add, + size: 20, + color: Theme.of(context).extension()!.iconsColor, + )); + } + + if (item is WalletAddressListItem) { + if (item.isHidden && !showHiddenAddresses) { + cell = Container(); + } else if (!item.isHidden && showHiddenAddresses) { + cell = Container(); + } else { cell = Observer(builder: (_) { - final isCurrent = item.address == addressListViewModel.address.address && editable; + final isCurrent = item.address == widget.addressListViewModel.address.address && editable; final backgroundColor = isCurrent ? Theme.of(context).extension()!.currentTileBackgroundColor : Theme.of(context).extension()!.tilesBackgroundColor; @@ -89,35 +156,51 @@ class AddressList extends StatelessWidget { ? Theme.of(context).extension()!.currentTileTextColor : Theme.of(context).extension()!.tilesTextColor; + return AddressCell.fromItem( item, isCurrent: isCurrent, - hasBalance: addressListViewModel.isElectrumWallet, - backgroundColor: backgroundColor, + hasBalance: widget.addressListViewModel.isBalanceAvailable, + hasReceived: widget.addressListViewModel.isReceivedAvailable, + // hasReceived: + backgroundColor: (kDebugMode && item.isHidden) ? + Theme.of(context).colorScheme.error : + (kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) : + backgroundColor, textColor: textColor, onTap: (_) { - if (onSelect != null) { - onSelect!(item.address); + if (widget.onSelect != null) { + widget.onSelect!(item.address); return; } - addressListViewModel.setAddress(item); + widget.addressListViewModel.setAddress(item); }, onEdit: editable - ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item) + ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) { + updateItems(); // refresh the new address + }) : null, + isHidden: item.isHidden, + onHide: () => _hideAddress(item), ); }); } + } - return index != 0 - ? cell - : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), topRight: Radius.circular(30)), - child: cell, - ); - }, - ), + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), topRight: Radius.circular(30)), + child: cell, + ); + }, ); } + + void _hideAddress(WalletAddressListItem item) async { + await widget.addressListViewModel.toggleHideAddress(item); + updateItems(); + } + } diff --git a/lib/src/screens/receive/widgets/header_tile.dart b/lib/src/screens/receive/widgets/header_tile.dart index faaa9ed07..dc48db89d 100644 --- a/lib/src/screens/receive/widgets/header_tile.dart +++ b/lib/src/screens/receive/widgets/header_tile.dart @@ -10,6 +10,7 @@ class HeaderTile extends StatefulWidget { this.showSearchButton = false, this.showTrailingButton = false, this.trailingButtonTap, + this.onSearchCallback, this.trailingIcon, }); @@ -18,6 +19,7 @@ class HeaderTile extends StatefulWidget { final bool showSearchButton; final bool showTrailingButton; final VoidCallback? trailingButtonTap; + final VoidCallback? onSearchCallback; final Icon? trailingIcon; @override @@ -41,7 +43,10 @@ class _HeaderTileState extends State { _isSearchActive ? Expanded( child: TextField( - onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value), + onChanged: (value) { + widget.walletAddressListViewModel.updateSearchText(value); + widget.onSearchCallback?.call(); + }, cursorColor: Theme.of(context).extension()!.tilesTextColor, cursorWidth: 0.5, decoration: InputDecoration( diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 9f0db059a..9d09e57a1 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -37,6 +37,10 @@ class QRWidget extends StatelessWidget { final int? qrVersion; final String? heroTag; + PaymentURI get addressUri { + return addressListViewModel.uri; + } + @override Widget build(BuildContext context) { final copyImage = Image.asset('assets/images/copy_address.png', @@ -77,14 +81,14 @@ class QRWidget extends StatelessWidget { () async { await Navigator.pushNamed(context, Routes.fullscreenQR, arguments: QrViewData( - data: addressListViewModel.uri.toString(), + data: addressUri.toString(), heroTag: heroTag, )); }, ); }, child: Hero( - tag: Key(heroTag ?? addressListViewModel.uri.toString()), + tag: Key(heroTag ?? addressUri.toString()), child: Center( child: AspectRatio( aspectRatio: 1.0, @@ -105,7 +109,7 @@ class QRWidget extends StatelessWidget { color: Colors.white, ), ), - child: QrImage(data: addressListViewModel.uri.toString())), + child: QrImage(data: addressUri.toString())), ), ), ), @@ -148,7 +152,7 @@ class QRWidget extends StatelessWidget { builder: (context) => Observer( builder: (context) => GestureDetector( onTap: () { - Clipboard.setData(ClipboardData(text: addressListViewModel.address.address)); + Clipboard.setData(ClipboardData(text: addressUri.address)); showBar(context, S.of(context).copied_to_clipboard); }, child: Row( @@ -157,7 +161,7 @@ class QRWidget extends StatelessWidget { children: [ Expanded( child: Text( - addressListViewModel.address.address, + addressUri.address, textAlign: TextAlign.center, style: TextStyle( fontSize: 15, diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index d63f78224..df6cbdb9f 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -27,32 +27,41 @@ abstract class ContactListViewModelBase with Store { settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) { - info.addressInfos!.forEach((key, value) { - final nextUnusedAddress = value.firstWhereOrNull( - (addressInfo) => !(info.usedAddresses?.contains(addressInfo.address) ?? false)); - if (nextUnusedAddress != null) { - final name = _createName(info.name, nextUnusedAddress.label); - walletContacts.add(WalletContact( - nextUnusedAddress.address, - name, - walletTypeToCryptoCurrency(info.type), - )); - } - }); + final key = info.addressInfos!.keys.first; + final value = info.addressInfos![key]; + final address = value?.first; + if (address != null) { + final name = _createName(info.name, address.label); + walletContacts.add(WalletContact( + address.address, + name, + walletTypeToCryptoCurrency(info.type), + )); + } } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { - info.addresses!.forEach((address, label) { - if (label.isEmpty) { - return; - } - final name = _createName(info.name, label); + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) { + final address = info.address; + final name = _createName(info.name, ""); walletContacts.add(WalletContact( address, name, - walletTypeToCryptoCurrency(info.type, - isTestnet: - info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + walletTypeToCryptoCurrency(info.type), )); - }); + } else { + info.addresses!.forEach((address, label) { + if (label.isEmpty) { + return; + } + final name = _createName(info.name, label); + walletContacts.add(WalletContact( + address, + name, + walletTypeToCryptoCurrency(info.type, + isTestnet: + info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + )); + }); + } } else { walletContacts.add(WalletContact( info.address, diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index ca56750f0..d29b7df6b 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -121,7 +121,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositAmount = ''; receiveAmount = ''; receiveAddress = ''; - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; provider = providersForCurrentPair().first; final initialProvider = provider; provider!.checkIsAvailable().then((bool isAvailable) { @@ -155,6 +155,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash; + bool get hideAddressAfterExchange => + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + bool _useTorOnly; final Box trades; final ExchangeTemplateStore _exchangeTemplateStore; @@ -540,6 +544,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with isFixedRate: isFixedRateMode, ); + if (hideAddressAfterExchange) { + wallet.walletAddresses.hiddenAddresses.add(depositAddress); + await wallet.walletAddresses.saveAddressesInBox(); + } + var amount = isFixedRateMode ? receiveAmount : depositAmount; amount = amount.replaceAll(',', '.'); @@ -603,8 +612,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with isReceiveAmountEntered = false; depositAmount = ''; receiveAmount = ''; - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : ''; - receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; + receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; isDepositAddressEnabled = !(depositCurrency == wallet.currency); isFixedRateMode = false; _onPairChange(); diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 2edda3d29..d365c8e00 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -78,6 +78,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { wallet, accountIndex: monero!.getCurrentAccount(wallet).id, label: label); + final addr = await monero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } @@ -88,6 +90,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { wallet, accountIndex: wownero!.getCurrentAccount(wallet).id, label: label); + final addr = await wownero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } diff --git a/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart b/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart new file mode 100644 index 000000000..74c7605e9 --- /dev/null +++ b/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart @@ -0,0 +1,3 @@ +import 'package:cake_wallet/utils/list_item.dart'; + +class WalletAddressHiddenListHeader extends ListItem {} \ No newline at end of file diff --git a/lib/view_model/wallet_address_list/wallet_address_list_item.dart b/lib/view_model/wallet_address_list/wallet_address_list_item.dart index 6a6e34113..725b1ddbf 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_item.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_item.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/utils/list_item.dart'; class WalletAddressListItem extends ListItem { - const WalletAddressListItem({ + WalletAddressListItem({ required this.address, required this.isPrimary, this.id, @@ -11,6 +11,8 @@ class WalletAddressListItem extends ListItem { this.isChange = false, // Address that is only ever used once, shouldn't be used to receive funds, copy and paste, share etc this.isOneTimeReceiveAddress = false, + this.isHidden = false, + this.isManual = false, }) : super(); final int? id; @@ -20,6 +22,8 @@ class WalletAddressListItem extends ListItem { final int? txCount; final String? balance; final bool isChange; + bool isHidden; + bool isManual; final bool? isOneTimeReceiveAddress; @override diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 17fc4b849..9fb7509eb 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -18,12 +18,16 @@ import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/api/wallet.dart'; +import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; @@ -271,56 +275,40 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType get type => wallet.type; @computed - WalletAddressListItem get address => - WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); + WalletAddressListItem get address { + return WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); + } @computed PaymentURI get uri { - if (wallet.type == WalletType.monero) { - return MoneroURI(amount: amount, address: address.address); + switch (wallet.type) { + case WalletType.monero: + return MoneroURI(amount: amount, address: address.address); + case WalletType.haven: + return HavenURI(amount: amount, address: address.address); + case WalletType.bitcoin: + return BitcoinURI(amount: amount, address: address.address); + case WalletType.litecoin: + return LitecoinURI(amount: amount, address: address.address); + case WalletType.ethereum: + return EthereumURI(amount: amount, address: address.address); + case WalletType.bitcoinCash: + return BitcoinCashURI(amount: amount, address: address.address); + case WalletType.banano: + return NanoURI(amount: amount, address: address.address); + case WalletType.nano: + return NanoURI(amount: amount, address: address.address); + case WalletType.polygon: + return PolygonURI(amount: amount, address: address.address); + case WalletType.solana: + return SolanaURI(amount: amount, address: address.address); + case WalletType.tron: + return TronURI(amount: amount, address: address.address); + case WalletType.wownero: + return WowneroURI(amount: amount, address: address.address); + case WalletType.none: + throw Exception('Unexpected type: ${type.toString()}'); } - - if (wallet.type == WalletType.haven) { - return HavenURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.bitcoin) { - return BitcoinURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.litecoin) { - return LitecoinURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.ethereum) { - return EthereumURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.bitcoinCash) { - return BitcoinCashURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.nano) { - return NanoURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.polygon) { - return PolygonURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.solana) { - return SolanaURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.tron) { - return TronURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.wownero) { - return WowneroURI(amount: amount, address: address.address); - } - - throw Exception('Unexpected type: ${type.toString()}'); } @computed @@ -341,7 +329,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo id: subaddress.id, isPrimary: isPrimary, name: subaddress.label, - address: subaddress.address); + address: subaddress.address, + balance: subaddress.received, + txCount: subaddress.txCount, + ); }); addressList.addAll(addressItems); } @@ -468,6 +459,16 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + for (var i = 0; i < addressList.length; i++) { + if (!(addressList[i] is WalletAddressListItem)) continue; + (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses.contains((addressList[i] as WalletAddressListItem).address); + } + + for (var i = 0; i < addressList.length; i++) { + if (!(addressList[i] is WalletAddressListItem)) continue; + (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses.contains((addressList[i] as WalletAddressListItem).address); + } + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { @@ -479,7 +480,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return addressList; } - + Future toggleHideAddress(WalletAddressListItem item) async { + if (item.isHidden) { + wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address); + } else { + wallet.walletAddresses.hiddenAddresses.add(item.address); + } + await wallet.walletAddresses.saveAddressesInBox(); + if (wallet.type == WalletType.monero) { + monero!.getSubaddressList(wallet).update(wallet, accountIndex: monero!.getCurrentAccount(wallet).id); + } else if (wallet.type == WalletType.wownero) { + wownero!.getSubaddressList(wallet).update(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id); + } else if (wallet.type == WalletType.haven) { + haven!.getSubaddressList(wallet).update(wallet, accountIndex: haven!.getCurrentAccount(wallet).id); + } + } @observable bool hasAccounts; @@ -515,6 +530,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash; + @computed + bool get isBalanceAvailable => isElectrumWallet; + + @computed + bool get isReceivedAvailable => + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + @computed bool get isSilentPayments => wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet); @@ -524,6 +547,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled && !isSilentPayments; + @computed + bool get showAddManualAddresses => + !isAutoGenerateSubaddressEnabled || + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + List _baseItems; final YatStore yatStore; @@ -542,6 +571,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void _init() { _baseItems = []; + if (wallet.walletAddresses.hiddenAddresses.isNotEmpty) { + _baseItems.add(WalletAddressHiddenListHeader()); + } + if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) { @@ -551,6 +584,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type != WalletType.nano && wallet.type != WalletType.banano) { _baseItems.add(WalletAddressListHeader()); } + if (wallet.isEnabledAutoGenerateSubaddress) { + wallet.walletAddresses.address = wallet.walletAddresses.latestAddress; + } } @action diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 0cb4b2f11..324db7548 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -333,6 +333,8 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "help", + "hide": "Hide", + "hidden_addresses": "Hidden Addresses", "hidden_balance": "Hidden Balance", "hide_details": "Hide Details", "high_contrast_theme": "High Contrast Theme", @@ -683,6 +685,7 @@ "setup_your_debit_card": "Set up your debit card", "share": "Share", "share_address": "Share address", + "show": "Show", "shared_seed_wallet_groups": "Shared Seed Wallet Groups", "show_details": "Show Details", "show_keys": "Show seed/keys", diff --git a/tool/configure.dart b/tool/configure.dart index 362a6d5d6..d54ec153d 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -303,10 +303,14 @@ class Subaddress { Subaddress({ required this.id, required this.label, - required this.address}); + required this.address, + required this.received, + required this.txCount}); final int id; final String label; final String address; + final String? received; + final int txCount; } class MoneroBalance extends Balance { From 651792a0fd7eda40fc504ba84d7a85e10f2ef2f4 Mon Sep 17 00:00:00 2001 From: Seth For Privacy <40500387+sethforprivacy@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:55:51 +0000 Subject: [PATCH 107/142] Clean up Silent Payments copy (#1724) --- res/values/strings_en.arb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 324db7548..19f939d19 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -145,7 +145,7 @@ "confirm_fee_deduction_content": "Do you agree to deduct the fee from the output?", "confirm_passphrase": "Confirm passphrase", "confirm_sending": "Confirm sending", - "confirm_silent_payments_switch_node": "Your current node does not support silent payments\\nCake Wallet will switch to a compatible node, just for scanning", + "confirm_silent_payments_switch_node": "Your current node does not support Silent Payments.\\n\\nCake Wallet will switch to a compatible node while scanning.", "confirmations": "Confirmations", "confirmed": "Confirmed Balance", "confirmed_tx": "Confirmed", @@ -236,7 +236,7 @@ "email_address": "Email Address", "enable_mempool_api": "Mempool API for accurate fees and dates", "enable_replace_by_fee": "Enable Replace-By-Fee", - "enable_silent_payments_scanning": "Start scanning silent payments, until the tip is reached", + "enable_silent_payments_scanning": "Start scanning for transactions sent to your Silent Payment address.", "enabled": "Enabled", "enter_amount": "Enter Amount", "enter_backup_password": "Enter backup password here", @@ -706,7 +706,7 @@ "silent_payments_disclaimer": "New addresses are not new identities. It is a re-use of an existing identity with a different label.", "silent_payments_display_card": "Show Silent Payments card", "silent_payments_scan_from_date": "Scan from date", - "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming silent payments, or, use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", + "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming Silent Payments or use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", "silent_payments_scan_from_height": "Scan from block height", "silent_payments_scanned_tip": "SCANNED TO TIP! (${tip})", "silent_payments_scanning": "Silent Payments Scanning", @@ -905,7 +905,7 @@ "warning": "Warning", "welcome": "Welcome to", "welcome_to_cakepay": "Welcome to Cake Pay!", - "what_is_silent_payments": "What is silent payments?", + "what_is_silent_payments": "What are Silent Payments?", "widgets_address": "Address", "widgets_or": "or", "widgets_restore_from_blockheight": "Restore from blockheight", @@ -934,4 +934,4 @@ "you_will_get": "Convert to", "you_will_send": "Convert from", "yy": "YY" -} \ No newline at end of file +} From 3348213e39ced300078b48c60cbbdee6796004c9 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 4 Oct 2024 19:38:07 +0300 Subject: [PATCH 108/142] fix min fee not met exception (#1726) --- cw_bitcoin/lib/exceptions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart index f7c593135..9bdb66eef 100644 --- a/cw_bitcoin/lib/exceptions.dart +++ b/cw_bitcoin/lib/exceptions.dart @@ -45,6 +45,6 @@ class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailed class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {} -class BitcoinTransactionCommitFailedLessThanMin extends TransactionCommitFailedBIP68Final {} +class BitcoinTransactionCommitFailedLessThanMin extends TransactionCommitFailedLessThanMin {} class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {} From fc14bf4e2b9f1f0906328481d7810e5e0a642b5a Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:39:29 +0100 Subject: [PATCH 109/142] fix: Issues with imaging (#1723) --- .../wallet_group_description_page.dart | 7 ---- .../new_wallet/wallet_group_display_page.dart | 41 ++++++++++++++----- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/lib/src/screens/new_wallet/wallet_group_description_page.dart b/lib/src/screens/new_wallet/wallet_group_description_page.dart index 5becea3a5..d0936b640 100644 --- a/lib/src/screens/new_wallet/wallet_group_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -7,7 +7,6 @@ import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:flutter_svg/svg.dart'; class WalletGroupDescriptionPage extends BasePage { WalletGroupDescriptionPage({required this.selectedWalletType}); @@ -21,12 +20,6 @@ class WalletGroupDescriptionPage extends BasePage { @override Widget body(BuildContext context) { - final lightImage = 'assets/images/wallet_group_light.png'; - final darkImage = 'assets/images/wallet_group_dark.png'; - final brightImage = 'assets/images/wallet_group_bright.png'; - - final image = currentTheme.type == ThemeType.light ? lightImage : darkImage; - return Container( alignment: Alignment.center, padding: EdgeInsets.all(24), diff --git a/lib/src/screens/new_wallet/wallet_group_display_page.dart b/lib/src/screens/new_wallet/wallet_group_display_page.dart index d3ce27446..a99d2bac7 100644 --- a/lib/src/screens/new_wallet/wallet_group_display_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -4,11 +4,11 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; - import '../../../themes/extensions/cake_text_theme.dart'; class WalletGroupsDisplayPage extends BasePage { @@ -16,22 +16,24 @@ class WalletGroupsDisplayPage extends BasePage { final WalletGroupsDisplayViewModel walletGroupsDisplayViewModel; - final walletTypeImage = Image.asset('assets/images/wallet_type.png'); - final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); - @override String get title => S.current.wallet_group; @override Widget body(BuildContext context) => WalletGroupsDisplayBody( walletGroupsDisplayViewModel: walletGroupsDisplayViewModel, + currentTheme: currentTheme, ); } class WalletGroupsDisplayBody extends StatelessWidget { - WalletGroupsDisplayBody({required this.walletGroupsDisplayViewModel}); + WalletGroupsDisplayBody({ + required this.walletGroupsDisplayViewModel, + required this.currentTheme, + }); final WalletGroupsDisplayViewModel walletGroupsDisplayViewModel; + final ThemeBase currentTheme; @override Widget build(BuildContext context) { @@ -47,7 +49,9 @@ class WalletGroupsDisplayBody extends StatelessWidget { return Column( children: [ if (walletGroupsDisplayViewModel.hasNoFilteredWallet) ...{ - WalletGroupEmptyStateWidget(), + WalletGroupEmptyStateWidget( + currentTheme: currentTheme, + ), }, ...walletGroupsDisplayViewModel.multiWalletGroups.map( (walletGroup) { @@ -153,17 +157,17 @@ class WalletGroupsDisplayBody extends StatelessWidget { } class WalletGroupEmptyStateWidget extends StatelessWidget { - const WalletGroupEmptyStateWidget({ - super.key, - }); + const WalletGroupEmptyStateWidget({required this.currentTheme, super.key}); + + final ThemeBase currentTheme; @override Widget build(BuildContext context) { return Column( children: [ Image.asset( - 'assets/images/wallet_group.png', - scale: 0.8, + _getThemedWalletGroupImage(currentTheme.type), + scale: 1.8, ), SizedBox(height: 32), Text.rich( @@ -190,4 +194,19 @@ class WalletGroupEmptyStateWidget extends StatelessWidget { ], ); } + + String _getThemedWalletGroupImage(ThemeType theme) { + final lightImage = 'assets/images/wallet_group_light.png'; + final darkImage = 'assets/images/wallet_group_dark.png'; + final brightImage = 'assets/images/wallet_group_bright.png'; + + switch (theme) { + case ThemeType.bright: + return brightImage; + case ThemeType.light: + return lightImage; + default: + return darkImage; + } + } } From 4b4d8a4840309ec746accbca82ea097d00a5bf6a Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 4 Oct 2024 20:01:46 +0300 Subject: [PATCH 110/142] Add passphrase support for Eth, Polygon, and Tron (#1719) * Add passphrase support for Eth, Polygon, and Tron * move passphrase to advanced settings even for restore --- .../lib/src/bitcoin_cash_wallet.dart | 2 +- .../lib/src/bitcoin_cash_wallet_service.dart | 2 +- cw_ethereum/lib/ethereum_wallet.dart | 5 +- cw_ethereum/lib/ethereum_wallet_service.dart | 2 + cw_evm/lib/evm_chain_wallet.dart | 16 +++- ...evm_chain_wallet_creation_credentials.dart | 25 +++--- cw_nano/lib/nano_wallet.dart | 4 + .../lib/nano_wallet_creation_credentials.dart | 6 +- cw_polygon/lib/polygon_wallet.dart | 5 +- cw_polygon/lib/polygon_wallet_service.dart | 2 + cw_solana/lib/solana_wallet.dart | 5 +- .../solana_wallet_creation_credentials.dart | 20 +++-- cw_tron/lib/tron_wallet.dart | 13 ++- .../lib/tron_wallet_creation_credentials.dart | 20 +++-- cw_tron/lib/tron_wallet_service.dart | 7 +- lib/entities/unstoppable_domain_address.dart | 3 - lib/ethereum/cw_ethereum.dart | 10 ++- lib/nano/cw_nano.dart | 4 + lib/polygon/cw_polygon.dart | 23 +++-- lib/solana/cw_solana.dart | 10 ++- .../advanced_privacy_settings_page.dart | 90 +++++++++---------- .../wallet_restore_from_seed_form.dart | 29 ------ .../screens/restore/wallet_restore_page.dart | 5 +- lib/tron/cw_tron.dart | 10 ++- .../advanced_privacy_settings_view_model.dart | 3 + .../restore/restore_from_qr_vm.dart | 39 ++++++-- lib/view_model/wallet_new_vm.dart | 5 ++ lib/view_model/wallet_restore_view_model.dart | 19 ++-- tool/configure.dart | 19 ++-- 29 files changed, 249 insertions(+), 154 deletions(-) diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index b1e5e7bf6..825c80d4a 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -83,7 +83,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, - seedBytes: await MnemonicBip39.toSeed(mnemonic, passphrase: passphrase), + seedBytes: MnemonicBip39.toSeed(mnemonic, passphrase: passphrase), encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index ab5117aa8..d14dc582d 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -36,7 +36,7 @@ class BitcoinCashWalletService extends WalletService< final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final wallet = await BitcoinCashWalletBase.create( - mnemonic: credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength), + mnemonic: credentials.mnemonic ?? MnemonicBip39.generate(strength: strength), password: credentials.password!, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index e8a9e85fa..765ace052 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -27,6 +27,7 @@ class EthereumWallet extends EVMChainWallet { super.initialBalance, super.privateKey, required super.encryptionFileUtils, + super.passphrase, }) : super(nativeCurrency: CryptoCurrency.eth); @override @@ -150,8 +151,9 @@ class EthereumWallet extends EVMChainWallet { if (!hasKeysFile) { final mnemonic = data!['mnemonic'] as String?; final privateKey = data['private_key'] as String?; + final passphrase = data['passphrase'] as String?; - keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase); } else { keysData = await WalletKeysFile.readKeysFile( name, @@ -166,6 +168,7 @@ class EthereumWallet extends EVMChainWallet { password: password, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, + passphrase: keysData.passphrase, initialBalance: balance, client: EthereumClient(), encryptionFileUtils: encryptionFileUtils, diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 51d6ecf42..858416055 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -27,6 +27,7 @@ class EthereumWalletService extends EVMChainWalletService { walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -144,6 +145,7 @@ class EthereumWalletService extends EVMChainWalletService { password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 0ade2215e..9bc18d14b 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -70,6 +70,7 @@ abstract class EVMChainWalletBase required String password, EVMChainERC20Balance? initialBalance, required this.encryptionFileUtils, + this.passphrase, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -178,6 +179,7 @@ abstract class EVMChainWalletBase mnemonic: _mnemonic, privateKey: _hexPrivateKey, password: _password, + passphrase: passphrase, ); walletAddresses.address = _evmChainPrivateKey.address.hexEip55; } @@ -545,6 +547,7 @@ abstract class EVMChainWalletBase 'mnemonic': _mnemonic, 'private_key': privateKey, 'balance': balance[currency]!.toJSON(), + 'passphrase': passphrase, }); Future _updateBalance() async { @@ -574,15 +577,19 @@ abstract class EVMChainWalletBase } } - Future getPrivateKey( - {String? mnemonic, String? privateKey, required String password}) async { + Future getPrivateKey({ + String? mnemonic, + String? privateKey, + required String password, + String? passphrase, + }) async { assert(mnemonic != null || privateKey != null); if (privateKey != null) { return EthPrivateKey.fromHex(privateKey); } - final seed = bip39.mnemonicToSeed(mnemonic!); + final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? ''); final root = bip32.BIP32.fromSeed(seed); @@ -716,4 +723,7 @@ abstract class EVMChainWalletBase @override String get password => _password; + + @override + final String? passphrase; } diff --git a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart index d7a33f6b3..5075e6289 100644 --- a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart +++ b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart @@ -4,28 +4,25 @@ import 'package:cw_core/wallet_info.dart'; class EVMChainNewWalletCredentials extends WalletCredentials { EVMChainNewWalletCredentials({ - required String name, - WalletInfo? walletInfo, - String? password, - String? parentAddress, + required super.name, + super.walletInfo, + super.password, + super.parentAddress, this.mnemonic, - }) : super( - name: name, - walletInfo: walletInfo, - password: password, - parentAddress: parentAddress, - ); + super.passphrase, + }); final String? mnemonic; } class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials { EVMChainRestoreWalletFromSeedCredentials({ - required String name, - required String password, + required super.name, + required super.password, required this.mnemonic, - WalletInfo? walletInfo, - }) : super(name: name, password: password, walletInfo: walletInfo); + super.walletInfo, + super.passphrase, + }); final String mnemonic; } diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 700710c2e..040fc9522 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -42,6 +42,7 @@ abstract class NanoWalletBase required String password, NanoBalance? initialBalance, required EncryptionFileUtils encryptionFileUtils, + this.passphrase, }) : syncStatus = NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -548,4 +549,7 @@ abstract class NanoWalletBase } return await NanoSignatures.verifyMessage(message, signature, address); } + + @override + final String? passphrase; } diff --git a/cw_nano/lib/nano_wallet_creation_credentials.dart b/cw_nano/lib/nano_wallet_creation_credentials.dart index 9958fad7c..59789aec7 100644 --- a/cw_nano/lib/nano_wallet_creation_credentials.dart +++ b/cw_nano/lib/nano_wallet_creation_credentials.dart @@ -9,13 +9,15 @@ class NanoNewWalletCredentials extends WalletCredentials { DerivationType? derivationType, this.mnemonic, String? parentAddress, + String? passphrase, }) : super( name: name, password: password, walletInfo: walletInfo, parentAddress: parentAddress, + passphrase: passphrase, ); - + final String? mnemonic; } @@ -25,10 +27,12 @@ class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { required this.mnemonic, String? password, required DerivationType derivationType, + String? passphrase, }) : super( name: name, password: password, derivationInfo: DerivationInfo(derivationType: derivationType), + passphrase: passphrase, ); final String mnemonic; diff --git a/cw_polygon/lib/polygon_wallet.dart b/cw_polygon/lib/polygon_wallet.dart index eb59a746e..f9aff16c3 100644 --- a/cw_polygon/lib/polygon_wallet.dart +++ b/cw_polygon/lib/polygon_wallet.dart @@ -27,6 +27,7 @@ class PolygonWallet extends EVMChainWallet { super.privateKey, required super.client, required super.encryptionFileUtils, + super.passphrase, }) : super(nativeCurrency: CryptoCurrency.maticpoly); @override @@ -128,8 +129,9 @@ class PolygonWallet extends EVMChainWallet { if (!hasKeysFile) { final mnemonic = data!['mnemonic'] as String?; final privateKey = data['private_key'] as String?; + final passphrase = data['passphrase'] as String?; - keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase); } else { keysData = await WalletKeysFile.readKeysFile( name, @@ -144,6 +146,7 @@ class PolygonWallet extends EVMChainWallet { password: password, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, + passphrase: keysData.passphrase, initialBalance: balance, client: PolygonClient(), encryptionFileUtils: encryptionFileUtils, diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index 514e73314..994912e8d 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -30,6 +30,7 @@ class PolygonWalletService extends EVMChainWalletService { walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -125,6 +126,7 @@ class PolygonWalletService extends EVMChainWalletService { password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 4e69db3b8..3c3078f2e 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -33,7 +33,6 @@ import 'package:solana/base58.dart'; import 'package:solana/metaplex.dart' as metaplex; import 'package:solana/solana.dart'; import 'package:solana/src/crypto/ed25519_hd_keypair.dart'; -import 'package:cryptography/cryptography.dart'; part 'solana_wallet.g.dart'; @@ -49,6 +48,7 @@ abstract class SolanaWalletBase required String password, SolanaBalance? initialBalance, required this.encryptionFileUtils, + this.passphrase, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -632,4 +632,7 @@ abstract class SolanaWalletBase @override String get password => _password; + + @override + final String? passphrase; } diff --git a/cw_solana/lib/solana_wallet_creation_credentials.dart b/cw_solana/lib/solana_wallet_creation_credentials.dart index f0901df79..121ef2b44 100644 --- a/cw_solana/lib/solana_wallet_creation_credentials.dart +++ b/cw_solana/lib/solana_wallet_creation_credentials.dart @@ -8,22 +8,30 @@ class SolanaNewWalletCredentials extends WalletCredentials { String? password, String? parentAddress, this.mnemonic, + String? passphrase, }) : super( name: name, walletInfo: walletInfo, password: password, parentAddress: parentAddress, + passphrase: passphrase, ); final String? mnemonic; } class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials { - SolanaRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required this.mnemonic, - WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + SolanaRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + String? passphrase, + }) : super( + name: name, + password: password, + walletInfo: walletInfo, + passphrase: passphrase, + ); final String mnemonic; } diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index f5841d894..9012ef528 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -47,6 +47,7 @@ abstract class TronWalletBase required String password, TronBalance? initialBalance, required this.encryptionFileUtils, + this.passphrase, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -113,6 +114,7 @@ abstract class TronWalletBase mnemonic: _mnemonic, privateKey: _hexPrivateKey, password: _password, + passphrase: passphrase, ); _tronPublicKey = _tronPrivateKey.publicKey(); @@ -149,8 +151,9 @@ abstract class TronWalletBase if (!hasKeysFile) { final mnemonic = data!['mnemonic'] as String?; final privateKey = data['private_key'] as String?; + final passphrase = data['passphrase'] as String?; - keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase); } else { keysData = await WalletKeysFile.readKeysFile( name, @@ -165,6 +168,7 @@ abstract class TronWalletBase password: password, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, + passphrase: keysData.passphrase, initialBalance: balance, encryptionFileUtils: encryptionFileUtils, ); @@ -190,12 +194,13 @@ abstract class TronWalletBase String? mnemonic, String? privateKey, required String password, + String? passphrase, }) async { assert(mnemonic != null || privateKey != null); if (privateKey != null) return TronPrivateKey(privateKey); - final seed = bip39.mnemonicToSeed(mnemonic!); + final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? ''); // Derive a TRON private key from the seed final bip44 = Bip44.fromSeed(seed, Bip44Coins.tron); @@ -463,6 +468,7 @@ abstract class TronWalletBase 'mnemonic': _mnemonic, 'private_key': privateKey, 'balance': balance[currency]!.toJSON(), + 'passphrase': passphrase, }); Future _updateBalance() async { @@ -607,4 +613,7 @@ abstract class TronWalletBase @override String get password => _password; + + @override + final String? passphrase; } diff --git a/cw_tron/lib/tron_wallet_creation_credentials.dart b/cw_tron/lib/tron_wallet_creation_credentials.dart index 5055c8a8d..fd9066acd 100644 --- a/cw_tron/lib/tron_wallet_creation_credentials.dart +++ b/cw_tron/lib/tron_wallet_creation_credentials.dart @@ -8,23 +8,31 @@ class TronNewWalletCredentials extends WalletCredentials { String? password, this.mnemonic, String? parentAddress, + String? passphrase, }) : super( name: name, walletInfo: walletInfo, password: password, parentAddress: parentAddress, + passphrase: passphrase, ); final String? mnemonic; } class TronRestoreWalletFromSeedCredentials extends WalletCredentials { - TronRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required this.mnemonic, - WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + TronRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + String? passphrase, + }) : super( + name: name, + password: password, + walletInfo: walletInfo, + passphrase: passphrase, + ); final String mnemonic; } diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart index 200653eb9..c732a87c4 100644 --- a/cw_tron/lib/tron_wallet_service.dart +++ b/cw_tron/lib/tron_wallet_service.dart @@ -33,10 +33,7 @@ class TronWalletService extends WalletService< WalletType getType() => WalletType.tron; @override - Future create( - TronNewWalletCredentials credentials, { - bool? isTestnet, - }) async { + Future create(TronNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); @@ -45,6 +42,7 @@ class TronWalletService extends WalletService< walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + passphrase: credentials.passphrase, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -120,6 +118,7 @@ class TronWalletService extends WalletService< password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/lib/entities/unstoppable_domain_address.dart b/lib/entities/unstoppable_domain_address.dart index 0f56517b8..6966fdd75 100644 --- a/lib/entities/unstoppable_domain_address.dart +++ b/lib/entities/unstoppable_domain_address.dart @@ -1,10 +1,7 @@ import 'dart:convert'; -import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; -const channel = MethodChannel('com.cake_wallet/native_utils'); - Future fetchUnstoppableDomainAddress(String domain, String ticker) async { var address = ''; diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 7796e1c60..e2c3d1358 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -14,6 +14,7 @@ class CWEthereum extends Ethereum { String? parentAddress, WalletInfo? walletInfo, String? password, + String? passphrase, }) => EVMChainNewWalletCredentials( name: name, @@ -21,6 +22,7 @@ class CWEthereum extends Ethereum { password: password, parentAddress: parentAddress, mnemonic: mnemonic, + passphrase: passphrase, ); @override @@ -28,8 +30,14 @@ class CWEthereum extends Ethereum { required String name, required String mnemonic, required String password, + String? passphrase, }) => - EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createEthereumRestoreWalletFromPrivateKey({ diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index 83702a8ce..9e47edc04 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -95,6 +95,7 @@ class CWNano extends Nano { String? password, String? mnemonic, String? parentAddress, + String? passphrase, }) => NanoNewWalletCredentials( name: name, @@ -102,6 +103,7 @@ class CWNano extends Nano { mnemonic: mnemonic, parentAddress: parentAddress, walletInfo: walletInfo, + passphrase: passphrase, ); @override @@ -110,6 +112,7 @@ class CWNano extends Nano { required String password, required String mnemonic, required DerivationType derivationType, + String? passphrase, }) { if (mnemonic.split(" ").length == 12 && derivationType != DerivationType.bip39) { throw Exception("Invalid mnemonic for derivation type!"); @@ -120,6 +123,7 @@ class CWNano extends Nano { password: password, mnemonic: mnemonic, derivationType: derivationType, + passphrase: passphrase, ); } diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 307107dd4..299c8278a 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -8,18 +8,21 @@ class CWPolygon extends Polygon { PolygonWalletService(walletInfoSource, isDirect, client: PolygonClient()); @override - WalletCredentials createPolygonNewWalletCredentials( - {required String name, - String? mnemonic, - String? parentAddress, - WalletInfo? walletInfo, - String? password}) => + WalletCredentials createPolygonNewWalletCredentials({ + required String name, + String? mnemonic, + String? parentAddress, + WalletInfo? walletInfo, + String? password, + String? passphrase, + }) => EVMChainNewWalletCredentials( name: name, walletInfo: walletInfo, password: password, mnemonic: mnemonic, parentAddress: parentAddress, + passphrase: passphrase, ); @override @@ -27,8 +30,14 @@ class CWPolygon extends Polygon { required String name, required String mnemonic, required String password, + String? passphrase, }) => - EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createPolygonRestoreWalletFromPrivateKey({ diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index 22cc478b6..7894f77ed 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -14,6 +14,7 @@ class CWSolana extends Solana { String? parentAddress, WalletInfo? walletInfo, String? password, + String? passphrase, }) => SolanaNewWalletCredentials( name: name, @@ -21,6 +22,7 @@ class CWSolana extends Solana { password: password, mnemonic: mnemonic, parentAddress: parentAddress, + passphrase: passphrase, ); @override @@ -28,8 +30,14 @@ class CWSolana extends Solana { required String name, required String mnemonic, required String password, + String? passphrase, }) => - SolanaRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + SolanaRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createSolanaRestoreWalletFromPrivateKey({ 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 8b7be18a1..26c96fb74 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -189,7 +189,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ), ); }), - if (!widget.isFromRestore) ...[ + if (!widget.isFromRestore) Observer(builder: (_) { if (widget.privacySettingsViewModel.hasSeedPhraseLengthOption) return SettingsPickerCell( @@ -202,54 +202,53 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ); return Container(); }), - if (widget.privacySettingsViewModel.hasPassphraseOption) - Padding( - padding: EdgeInsets.all(24), - child: Form( - key: _passphraseFormKey, - child: Column( - children: [ - BaseTextFormField( - hintText: S.of(context).passphrase, - controller: passphraseController, - obscureText: obscurePassphrase, - suffixIcon: GestureDetector( - onTap: () => setState(() { - obscurePassphrase = !obscurePassphrase; - }), - child: Icon( - Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, - ), + if (widget.privacySettingsViewModel.hasPassphraseOption) + Padding( + padding: EdgeInsets.all(24), + child: Form( + key: _passphraseFormKey, + child: Column( + children: [ + BaseTextFormField( + hintText: S.of(context).passphrase, + controller: passphraseController, + obscureText: obscurePassphrase, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, ), ), - const SizedBox(height: 10), - BaseTextFormField( - hintText: S.of(context).confirm_passphrase, - controller: confirmPassphraseController, - obscureText: obscurePassphrase, - validator: (text) { - if (text == passphraseController.text) { - return null; - } + ), + const SizedBox(height: 10), + BaseTextFormField( + hintText: S.of(context).confirm_passphrase, + controller: confirmPassphraseController, + obscureText: obscurePassphrase, + validator: (text) { + if (text == passphraseController.text) { + return null; + } - return S.of(context).passphrases_doesnt_match; - }, - suffixIcon: GestureDetector( - onTap: () => setState(() { - obscurePassphrase = !obscurePassphrase; - }), - child: Icon( - Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, - ), + return S.of(context).passphrases_doesnt_match; + }, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, ), ), - ], - ), + ), + ], ), ), - ], + ), Observer(builder: (_) { return Column( children: [ @@ -311,13 +310,14 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo widget.nodeViewModel.save(); } if (passphraseController.text.isNotEmpty) { - if (_passphraseFormKey.currentState != null && !_passphraseFormKey.currentState!.validate()) { + if (_passphraseFormKey.currentState != null && + !_passphraseFormKey.currentState!.validate()) { return; } - - widget.seedTypeViewModel.setPassphrase(passphraseController.text); } + widget.seedTypeViewModel.setPassphrase(passphraseController.text); + Navigator.pop(context); }, text: S.of(context).continue_text, 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 897a6bed0..c8a75cf40 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -19,7 +19,6 @@ class WalletRestoreFromSeedForm extends StatefulWidget { WalletRestoreFromSeedForm({Key? key, required this.displayLanguageSelector, required this.displayBlockHeightSelector, - required this.displayPassphrase, required this.type, required this.displayWalletPassword, required this.seedSettingsViewModel, @@ -35,7 +34,6 @@ class WalletRestoreFromSeedForm extends StatefulWidget { final bool displayLanguageSelector; final bool displayBlockHeightSelector; final bool displayWalletPassword; - final bool displayPassphrase; final SeedSettingsViewModel seedSettingsViewModel; final FocusNode? blockHeightFocusNode; final Function(bool)? onHeightOrDateEntered; @@ -60,7 +58,6 @@ class WalletRestoreFromSeedFormState extends State { repeatedPasswordTextEditingController = displayWalletPassword ? TextEditingController() : null, - passphraseController = TextEditingController(), seedTypeController = TextEditingController(); final GlobalKey seedWidgetStateKey; @@ -70,15 +67,11 @@ class WalletRestoreFromSeedFormState extends State { final TextEditingController? passwordTextEditingController; final TextEditingController? repeatedPasswordTextEditingController; final TextEditingController seedTypeController; - final TextEditingController passphraseController; final GlobalKey formKey; late ReactionDisposer moneroSeedTypeReaction; String language; void Function()? passwordListener; void Function()? repeatedPasswordListener; - void Function()? passphraseListener; - - bool obscurePassphrase = true; @override void initState() { @@ -96,9 +89,6 @@ class WalletRestoreFromSeedFormState extends State { repeatedPasswordTextEditingController?.addListener(repeatedPasswordListener!); } - passphraseListener = () => widget.seedSettingsViewModel.setPassphrase(passphraseController.text); - passphraseController.addListener(passphraseListener!); - moneroSeedTypeReaction = reaction((_) => widget.seedSettingsViewModel.moneroSeedType, (MoneroSeedType item) { _setSeedType(item); @@ -120,8 +110,6 @@ class WalletRestoreFromSeedFormState extends State { repeatedPasswordTextEditingController?.removeListener(repeatedPasswordListener!); } - passphraseController.removeListener(passphraseListener!); - super.dispose(); } @@ -280,23 +268,6 @@ class WalletRestoreFromSeedFormState extends State { hasDatePicker: widget.type == WalletType.monero || widget.type == WalletType.wownero, walletType: widget.type, ), - if (widget.displayPassphrase) ...[ - const SizedBox(height: 10), - BaseTextFormField( - hintText: S.current.passphrase, - controller: passphraseController, - obscureText: obscurePassphrase, - suffixIcon: GestureDetector( - onTap: () => setState(() { - obscurePassphrase = !obscurePassphrase; - }), - child: Icon( - Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, - ), - ), - ), - ] ])); } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 4a4e27b3d..6215e26c3 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -37,7 +37,6 @@ class WalletRestorePage extends BasePage { displayBlockHeightSelector: walletRestoreViewModel.hasBlockchainHeightLanguageSelector, displayLanguageSelector: walletRestoreViewModel.hasSeedLanguageSelector, - displayPassphrase: walletRestoreViewModel.hasPassphrase, type: walletRestoreViewModel.type, key: walletRestoreFromSeedFormKey, blockHeightFocusNode: _blockHeightFocusNode, @@ -320,9 +319,7 @@ class WalletRestorePage extends BasePage { -1; } - if (walletRestoreViewModel.hasPassphrase) { - credentials['passphrase'] = seedSettingsViewModel.passphrase; - } + credentials['passphrase'] = seedSettingsViewModel.passphrase; credentials['name'] = walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text; diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index b4aed4235..8bceafe01 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -15,12 +15,14 @@ class CWTron extends Tron { String? password, String? mnemonic, String? parentAddress, + String? passphrase, }) => TronNewWalletCredentials( name: name, walletInfo: walletInfo, password: password, mnemonic: mnemonic, + passphrase: passphrase, parentAddress: parentAddress); @override @@ -28,8 +30,14 @@ class CWTron extends Tron { required String name, required String mnemonic, required String password, + String? passphrase, }) => - TronRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + TronRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createTronRestoreWalletFromPrivateKey({ diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index b97c796f7..85b9dbead 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -75,6 +75,9 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash, + WalletType.ethereum, + WalletType.polygon, + WalletType.tron, ].contains(type); @computed diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 6701b639d..798105b9d 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -13,7 +13,7 @@ import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cw_core/wallet_credentials.dart'; @@ -26,14 +26,19 @@ part 'restore_from_qr_vm.g.dart'; class WalletRestorationFromQRVM = WalletRestorationFromQRVMBase with _$WalletRestorationFromQRVM; abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store { - WalletRestorationFromQRVMBase(AppStore appStore, WalletCreationService walletCreationService, - Box walletInfoSource, WalletType type, SeedSettingsViewModel seedSettingsViewModel) + WalletRestorationFromQRVMBase( + AppStore appStore, + WalletCreationService walletCreationService, + Box walletInfoSource, + WalletType type, + SeedSettingsViewModel seedSettingsViewModel) : height = 0, viewKey = '', spendKey = '', wif = '', address = '', - super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true); + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: true); @observable int height; @@ -124,26 +129,44 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, + passphrase: restoreWallet.passphrase, ); case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.nano: return nano!.createNanoRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, derivationType: derivationInfo!.derivationType!, + passphrase: restoreWallet.passphrase, ); case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.tron: return tron!.createTronRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.wownero: return wownero!.createWowneroRestoreWalletFromSeedCredentials( name: name, diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 2919afcac..be30811d9 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -106,6 +106,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashNewWalletCredentials( @@ -122,6 +123,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.polygon: return polygon!.createPolygonNewWalletCredentials( @@ -129,6 +131,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.solana: return solana!.createSolanaNewWalletCredentials( @@ -136,6 +139,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.tron: return tron!.createTronNewWalletCredentials( @@ -143,6 +147,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.wownero: return wownero!.createWowneroNewWalletCredentials( diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 5462ce4c8..d37b69f74 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -78,9 +78,6 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final bool hasBlockchainHeightLanguageSelector; final bool hasRestoreFromPrivateKey; - bool get hasPassphrase => - [WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(type); - @observable WalletRestoreMode mode; @@ -116,10 +113,18 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { name: name, height: height, mnemonic: seed, password: password); case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( - name: name, mnemonic: seed, password: password); + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( - name: name, mnemonic: seed, password: password); + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); case WalletType.nano: case WalletType.banano: return nano!.createNanoRestoreWalletFromSeedCredentials( @@ -127,24 +132,28 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, derivationType: derivationInfo!.derivationType!, + passphrase: passphrase, ); case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.tron: return tron!.createTronRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.wownero: return wownero!.createWowneroRestoreWalletFromSeedCredentials( diff --git a/tool/configure.dart b/tool/configure.dart index d54ec153d..c3d84c4f6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -843,8 +843,8 @@ import 'package:eth_sig_util/util/utils.dart'; abstract class Ethereum { List getEthereumWordList(String language); WalletService createEthereumWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress}); - WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createEthereumRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createEthereumHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); String getAddress(WalletBase wallet); @@ -947,8 +947,8 @@ import 'package:eth_sig_util/util/utils.dart'; abstract class Polygon { List getPolygonWordList(String language); WalletService createPolygonWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress}); - WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createPolygonRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createPolygonHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); String getAddress(WalletBase wallet); @@ -1119,6 +1119,7 @@ abstract class Nano { String? mnemonic, String? parentAddress, WalletInfo? walletInfo, + String? passphrase, }); WalletCredentials createNanoRestoreWalletFromSeedCredentials({ @@ -1126,6 +1127,7 @@ abstract class Nano { required String password, required String mnemonic, required DerivationType derivationType, + String? passphrase, }); WalletCredentials createNanoRestoreWalletFromKeysCredentials({ @@ -1234,9 +1236,9 @@ abstract class Solana { List getSolanaWordList(String language); WalletService createSolanaWalletService(Box walletInfoSource, bool isDirect); WalletCredentials createSolanaNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress,}); + {required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); WalletCredentials createSolanaRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}); + {required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createSolanaRestoreWalletFromPrivateKey( {required String name, required String privateKey, required String password}); @@ -1320,9 +1322,8 @@ import 'package:cw_tron/tron_wallet_service.dart'; abstract class Tron { List getTronWordList(String language); WalletService createTronWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, - String? parentAddress}); - WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createTronRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); String getAddress(WalletBase wallet); From d933743a72be1e5c1031c82447cb2d104ba498f2 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 4 Oct 2024 22:04:23 +0300 Subject: [PATCH 111/142] Cw 772 restore from qr generates different wallet (#1721) * fix derivation info for QR restoring * allow all available seed languages for Monero --------- Co-authored-by: Omar Hatem --- .../restore/restore_from_qr_vm.dart | 21 +++--- .../restore/wallet_restore_from_qr_code.dart | 4 ++ lib/view_model/wallet_creation_vm.dart | 64 ++++++++----------- 3 files changed, 40 insertions(+), 49 deletions(-) diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 798105b9d..042a048d9 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -37,8 +37,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store spendKey = '', wif = '', address = '', - super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, - type: type, isRecovery: true); + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true); @observable int height; @@ -58,13 +57,10 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store bool get hasRestorationHeight => type == WalletType.monero || type == WalletType.wownero; @override - WalletCredentials getCredentialsFromRestoredWallet( - dynamic options, RestoredWallet restoreWallet) { + Future getWalletCredentialsFromQRCredentials( + RestoredWallet restoreWallet) async { final password = generateWalletPassword(); - DerivationInfo? derivationInfo; - derivationInfo ??= getDefaultCreateDerivation(); - switch (restoreWallet.restoreMode) { case WalletRestoreMode.keys: switch (restoreWallet.type) { @@ -116,12 +112,13 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store ); case WalletType.bitcoin: case WalletType.litecoin: + final derivationInfo = (await getDerivationInfoFromQRCredentials(restoreWallet)).first; return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, passphrase: restoreWallet.passphrase, - derivationType: derivationInfo!.derivationType!, + derivationType: derivationInfo.derivationType!, derivationPath: derivationInfo.derivationPath!, ); case WalletType.bitcoinCash: @@ -139,11 +136,13 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store passphrase: restoreWallet.passphrase, ); case WalletType.nano: + final derivationInfo = + (await getDerivationInfoFromQRCredentials(restoreWallet)).first; return nano!.createNanoRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, - derivationType: derivationInfo!.derivationType!, + derivationType: derivationInfo.derivationType!, passphrase: restoreWallet.passphrase, ); case WalletType.polygon: @@ -183,8 +182,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store } @override - Future processFromRestoredWallet( - WalletCredentials credentials, RestoredWallet restoreWallet) async { + Future processFromRestoredWallet(WalletCredentials credentials, + RestoredWallet restoreWallet) async { try { switch (restoreWallet.restoreMode) { case WalletRestoreMode.keys: diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 335b1a006..23850befa 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -142,6 +142,10 @@ class WalletRestoreFromQRCode { return WalletRestoreMode.seed; } + if ((type == WalletType.monero || type == WalletType.wownero)) { + return WalletRestoreMode.seed; + } + seedValue.split(' ').forEach((element) { if (!words.contains(element)) { throw Exception( diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 9fce86712..13c1a5661 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -85,21 +85,9 @@ abstract class WalletCreationVMBase with Store { final dirPath = await pathForWalletDir(name: name, type: type); final path = await pathForWallet(name: name, type: type); - WalletCredentials credentials; - if (restoreWallet != null) { - if (restoreWallet.restoreMode == WalletRestoreMode.seed && - options == null && - (type == WalletType.nano || - type == WalletType.bitcoin || - type == WalletType.litecoin)) { - final derivationInfo = await getDerivationInfo(restoreWallet); - options ??= {}; - options["derivationInfo"] = derivationInfo.first; - } - credentials = getCredentialsFromRestoredWallet(options, restoreWallet); - } else { - credentials = getCredentials(options); - } + final credentials = restoreWallet != null + ? await getWalletCredentialsFromQRCredentials(restoreWallet) + : getCredentials(options); final walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -200,36 +188,36 @@ abstract class WalletCreationVMBase with Store { } } - Future> getDerivationInfo(RestoredWallet restoreWallet) async { - var list = []; - final walletType = restoreWallet.type; - var appStore = getIt.get(); - var node = appStore.settingsStore.getCurrentNode(walletType); + Future> getDerivationInfoFromQRCredentials(RestoredWallet restoreWallet) async { + var list = []; + final walletType = restoreWallet.type; + var appStore = getIt.get(); + var node = appStore.settingsStore.getCurrentNode(walletType); - switch (walletType) { - case WalletType.bitcoin: - case WalletType.litecoin: - return bitcoin!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - case WalletType.nano: - return nanoUtil!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - default: - break; + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + return bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; } - return list; - } WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); Future process(WalletCredentials credentials) => throw UnimplementedError(); - WalletCredentials getCredentialsFromRestoredWallet( - dynamic options, RestoredWallet restoreWallet) => + Future getWalletCredentialsFromQRCredentials(RestoredWallet restoreWallet) async => throw UnimplementedError(); Future processFromRestoredWallet( From 6dba73a1d5b627a8bb4566f273aec63111a90cb3 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 4 Oct 2024 23:00:36 +0300 Subject: [PATCH 112/142] Generic fixes (#1716) * reset brightness after using * set default qr to auto detect * minor safety enhancement * close socket after checking connectivity * fix incorrect parent exception class * pump medium and fast priorities even if they are equal to slow * Add Ton to exchange * fix silent payments add Seth bitcoin node * deselect quantex add seth node for existing users * fix node uri --- assets/bitcoin_electrum_server_list.yml | 3 + assets/images/ton_icon.png | Bin 0 -> 14318 bytes cw_bitcoin/lib/electrum_wallet.dart | 48 +++++++++------- cw_bitcoin/pubspec.lock | 39 +++++++++---- cw_core/lib/crypto_currency.dart | 2 + cw_core/lib/node.dart | 7 ++- lib/entities/default_settings_migration.dart | 52 ++++++++++++++++-- lib/main.dart | 2 +- lib/reactions/check_connection.dart | 2 + lib/src/screens/receive/widgets/qr_image.dart | 2 +- lib/utils/brightness_util.dart | 5 +- 11 files changed, 119 insertions(+), 43 deletions(-) create mode 100644 assets/images/ton_icon.png diff --git a/assets/bitcoin_electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml index 8b734a7bb..305db38f5 100644 --- a/assets/bitcoin_electrum_server_list.yml +++ b/assets/bitcoin_electrum_server_list.yml @@ -6,3 +6,6 @@ isDefault: true - uri: electrs.cakewallet.com:50001 +- + uri: fulcrum.sethforprivacy.com:50002 + useSSL: true diff --git a/assets/images/ton_icon.png b/assets/images/ton_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..90f9968cccff796368b266a6f497fdd7403091b3 GIT binary patch literal 14318 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 YCEguol^6iv2LH6xZ{B!+9TWcl0QGAZ7ytkO literal 0 HcmV?d00001 diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4f8f3eab5..877b108b7 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -479,8 +479,14 @@ abstract class ElectrumWalletBase final result = json.decode(response.body) as Map; final slowFee = result['economyFee']?.toInt() ?? 0; - final mediumFee = result['hourFee']?.toInt() ?? 0; - final fastFee = result['fastestFee']?.toInt() ?? 0; + int mediumFee = result['hourFee']?.toInt() ?? 0; + int fastFee = result['fastestFee']?.toInt() ?? 0; + if (slowFee == mediumFee) { + mediumFee++; + } + while (fastFee <= mediumFee) { + fastFee++; + } _feeRates = [slowFee, mediumFee, fastFee]; return; } catch (_) {} @@ -813,6 +819,8 @@ abstract class ElectrumWalletBase network: network, memo: memo, feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, ); if (fee == 0) { @@ -1639,27 +1647,29 @@ abstract class ElectrumWalletBase if (verboseTransaction.isEmpty) { transactionHex = await electrumClient.getTransactionHex(hash: hash); - if (height != null && await checkIfMempoolAPIIsEnabled()) { - final blockHash = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", - ), - ); - - if (blockHash.statusCode == 200 && - blockHash.body.isNotEmpty && - jsonDecode(blockHash.body) != null) { - final blockResponse = await http.get( + if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { + try { + final blockHash = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", ), ); - if (blockResponse.statusCode == 200 && - blockResponse.body.isNotEmpty && - jsonDecode(blockResponse.body)['timestamp'] != null) { - time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + + if (blockHash.statusCode == 200 && + blockHash.body.isNotEmpty && + jsonDecode(blockHash.body) != null) { + final blockResponse = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + ), + ); + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + } } - } + } catch (_) {} } } else { transactionHex = verboseTransaction['hex'] as String; diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index f741230a5..4c7c49a9e 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -49,6 +49,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bech32: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" bip32: dependency: transitive description: @@ -78,8 +87,8 @@ packages: dependency: "direct overridden" description: path: "." - ref: cake-update-v7 - resolved-ref: f577e83fe78766b2655ea0602baa9299b953a31b + ref: cake-update-v8 + resolved-ref: fc045a11db3d85d806ca67f75e8b916c706745a2 url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" @@ -308,13 +317,13 @@ packages: source: hosted version: "1.3.1" ffi: - dependency: transitive + dependency: "direct overridden" description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.0" ffigen: dependency: transitive description: @@ -450,6 +459,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http2: + dependency: transitive + description: + name: http2 + sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d" + url: "https://pub.dev" + source: hosted + version: "2.3.0" http_multi_server: dependency: transitive description: @@ -693,13 +710,13 @@ packages: source: hosted version: "2.1.8" pointycastle: - dependency: transitive + dependency: "direct overridden" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "3.7.4" pool: dependency: transitive description: @@ -709,13 +726,13 @@ packages: source: hosted version: "1.5.1" protobuf: - dependency: transitive + dependency: "direct overridden" description: name: protobuf - sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08" + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.1.0" provider: dependency: transitive description: diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 4f9b5d835..0280bb45a 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -106,6 +106,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.usdcTrc20, CryptoCurrency.tbtc, CryptoCurrency.wow, + CryptoCurrency.ton, ]; static const havenCurrencies = [ @@ -223,6 +224,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const tbtc = CryptoCurrency(title: 'tBTC', fullName: 'Testnet Bitcoin', raw: 93, name: 'tbtc', iconPath: 'assets/images/tbtc.png', decimals: 8); static const wow = CryptoCurrency(title: 'WOW', fullName: 'Wownero', raw: 94, name: 'wow', iconPath: 'assets/images/wownero_icon.png', decimals: 11); + static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8); static final Map _rawCurrencyMap = diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 85c61de15..e19d2a54b 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -239,12 +239,15 @@ class Node extends HiveObject with Keyable { // you try to communicate with it Future requestElectrumServer() async { try { + final Socket socket; if (useSSL == true) { - await SecureSocket.connect(uri.host, uri.port, + socket = await SecureSocket.connect(uri.host, uri.port, timeout: Duration(seconds: 5), onBadCertificate: (_) => true); } else { - await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); + socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); } + + socket.destroy(); return true; } catch (_) { return false; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 802ad0184..1c46cd605 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io' show Directory, File, Platform; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/secure_storage.dart'; @@ -234,7 +235,8 @@ Future defaultSettingsMigration( break; case 36: await addWowneroNodeList(nodes: nodes); - await changeWowneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await changeWowneroCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; case 37: await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); @@ -249,6 +251,10 @@ Future defaultSettingsMigration( case 40: await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); break; + case 41: + _deselectQuantex(sharedPreferences); + await _addSethNode(nodes, sharedPreferences); + break; default: break; } @@ -263,6 +269,19 @@ Future defaultSettingsMigration( await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); } +void _deselectQuantex(SharedPreferences sharedPreferences) { + final Map exchangeProvidersSelection = + json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") + as Map; + + exchangeProvidersSelection['Quantex'] = false; + + sharedPreferences.setString( + PreferencesKey.exchangeProvidersSelection, + json.encode(exchangeProvidersSelection), + ); +} + void _fixNodesUseSSLFlag(Box nodes) { for (Node node in nodes.values) { switch (node.uriRaw) { @@ -887,7 +906,30 @@ Future changeDefaultBitcoinNode( } } +Future _addSethNode(Box nodeSource, SharedPreferences sharedPreferences) async { + _addBitcoinNode( + nodeSource: nodeSource, + sharedPreferences: sharedPreferences, + nodeUri: "fulcrum.sethforprivacy.com:50002", + useSSL: false, + ); +} + Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { + _addBitcoinNode( + nodeSource: nodeSource, + sharedPreferences: sharedPreferences, + nodeUri: cakeWalletSilentPaymentsElectrsUri, + ); +} + +Future _addBitcoinNode({ + required Box nodeSource, + required SharedPreferences sharedPreferences, + required String nodeUri, + bool replaceExisting = false, + bool useSSL = false, +}) async { const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; final currentBitcoinNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); @@ -896,12 +938,11 @@ Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPrefe final needToReplaceCurrentBitcoinNode = currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - final newElectRsBitcoinNode = - Node(uri: cakeWalletSilentPaymentsElectrsUri, type: WalletType.bitcoin, useSSL: false); + final newElectRsBitcoinNode = Node(uri: nodeUri, type: WalletType.bitcoin, useSSL: useSSL); await nodeSource.add(newElectRsBitcoinNode); - if (needToReplaceCurrentBitcoinNode) { + if (needToReplaceCurrentBitcoinNode && replaceExisting) { await sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, newElectRsBitcoinNode.key as int); } @@ -1262,7 +1303,8 @@ Future removeMoneroWorld( const cakeWalletMoneroNodeUriPattern = '.moneroworld.com'; final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final currentMoneroNode = nodes.values.firstWhere((node) => node.key == currentMoneroNodeId); - final needToReplaceCurrentMoneroNode = currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); + final needToReplaceCurrentMoneroNode = + currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); nodes.values.forEach((node) async { if (node.type == WalletType.monero && diff --git a/lib/main.dart b/lib/main.dart index dcfd8d0da..7237de8b9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -192,7 +192,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 40, + initialMigrationVersion: 41, ); } diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index 812fa9fcd..570b96864 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -11,6 +11,8 @@ Timer? _checkConnectionTimer; void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore, {int timeInterval = 5}) { _checkConnectionTimer?.cancel(); + // TODO: check the validity of this code, and if it's working fine, then no need for + // having the connect function in electrum.dart when the syncstatus is lost or failed and add the not connected state _checkConnectionTimer = Timer.periodic(Duration(seconds: timeInterval), (_) async { if (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus) { return; diff --git a/lib/src/screens/receive/widgets/qr_image.dart b/lib/src/screens/receive/widgets/qr_image.dart index f388fdd0b..043958ef4 100644 --- a/lib/src/screens/receive/widgets/qr_image.dart +++ b/lib/src/screens/receive/widgets/qr_image.dart @@ -23,7 +23,7 @@ class QrImage extends StatelessWidget { return qr.QrImageView( data: data, errorCorrectionLevel: errorCorrectionLevel, - version: version ?? 9, // Previous value: 7 something happened after flutter upgrade monero wallets addresses are longer than ver. 7 ??? + version: version ?? qr.QrVersions.auto, size: size, foregroundColor: foregroundColor, backgroundColor: backgroundColor, diff --git a/lib/utils/brightness_util.dart b/lib/utils/brightness_util.dart index 5afe065e5..1b34bceac 100644 --- a/lib/utils/brightness_util.dart +++ b/lib/utils/brightness_util.dart @@ -9,15 +9,12 @@ class BrightnessUtil { return; } - // Get the current brightness: - final brightness = await DeviceDisplayBrightness.getBrightness(); - // ignore: unawaited_futures DeviceDisplayBrightness.setBrightness(1.0); await func(); // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(brightness); + DeviceDisplayBrightness.resetBrightness(); } } \ No newline at end of file From dfccedddb21b9581fe45b9a35ca5d1b4bacdec11 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 5 Oct 2024 00:50:36 +0300 Subject: [PATCH 113/142] Cw 769 fix transaction notes not showing (#1718) * use focusNode instead of onTapOutside for TextFieldListRow * add a transaction description box to the backup * fix --------- Co-authored-by: OmarHatem --- lib/core/backup_service.dart | 33 ++++- lib/di.dart | 1 + lib/entities/transaction_description.dart | 14 +++ .../widgets/textfield_list_row.dart | 117 +++++++++++------- .../unspent_coins_details_page.dart | 1 - 5 files changed, 117 insertions(+), 49 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 42e24d3c7..26e2136dc 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -2,14 +2,13 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cake_wallet/utils/device_info.dart'; -import 'package:cw_core/root_dir.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:archive/archive_io.dart'; @@ -24,8 +23,8 @@ import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cake_backup/backup.dart' as cake_backup; class BackupService { - BackupService( - this._secureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) + BackupService(this._secureStorage, this._walletInfoSource, this._transactionDescriptionBox, + this._keyService, this._sharedPreferences) : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = []; @@ -38,6 +37,7 @@ class BackupService { final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final Box _walletInfoSource; + final Box _transactionDescriptionBox; final KeyService _keyService; List _correctWallets; @@ -86,6 +86,13 @@ class BackupService { final preferencesDump = await _exportPreferencesJSON(); final preferencesDumpFile = File('${tmpDir.path}/~_preferences_dump_TMP'); final keychainDumpFile = File('${tmpDir.path}/~_keychain_dump_TMP'); + final transactionDescriptionDumpFile = + File('${tmpDir.path}/~_transaction_descriptions_dump_TMP'); + + final transactionDescriptionData = _transactionDescriptionBox + .toMap() + .map((key, value) => MapEntry(key.toString(), value.toJson())); + final transactionDescriptionDump = jsonEncode(transactionDescriptionData); if (tmpDir.existsSync()) { tmpDir.deleteSync(recursive: true); @@ -107,8 +114,10 @@ class BackupService { }); await keychainDumpFile.writeAsBytes(keychainDump.toList()); await preferencesDumpFile.writeAsString(preferencesDump); + await transactionDescriptionDumpFile.writeAsString(transactionDescriptionDump); await zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump'); await zipEncoder.addFile(keychainDumpFile, '~_keychain_dump'); + await zipEncoder.addFile(transactionDescriptionDumpFile, '~_transaction_descriptions_dump'); zipEncoder.close(); final content = File(archivePath).readAsBytesSync(); @@ -160,6 +169,7 @@ class BackupService { await _verifyWallets(); await _importKeychainDumpV2(password); await _importPreferencesDump(); + await _importTransactionDescriptionDump(); } Future _verifyWallets() async { @@ -184,6 +194,21 @@ class BackupService { return await CakeHive.openBox(WalletInfo.boxName); } + Future _importTransactionDescriptionDump() async { + final appDir = await getAppDir(); + final transactionDescriptionFile = File('${appDir.path}/~_transaction_descriptions_dump'); + + if (!transactionDescriptionFile.existsSync()) { + return; + } + + final jsonData = + json.decode(transactionDescriptionFile.readAsStringSync()) as Map; + final descriptionsMap = jsonData.map((key, value) => + MapEntry(key, TransactionDescription.fromJson(value as Map))); + await _transactionDescriptionBox.putAll(descriptionsMap); + } + Future _importPreferencesDump() async { final appDir = await getAppDir(); final preferencesFile = File('${appDir.path}/~_preferences_dump'); diff --git a/lib/di.dart b/lib/di.dart index 4a1952729..5e5417b34 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1149,6 +1149,7 @@ Future setup({ getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); getIt.registerFactory(() => BackupService(getIt.get(), _walletInfoSource, + _transactionDescriptionBox, getIt.get(), getIt.get())); getIt.registerFactory(() => BackupViewModel( diff --git a/lib/entities/transaction_description.dart b/lib/entities/transaction_description.dart index 088f9c480..2ac573652 100644 --- a/lib/entities/transaction_description.dart +++ b/lib/entities/transaction_description.dart @@ -21,4 +21,18 @@ class TransactionDescription extends HiveObject { String? transactionNote; String get note => transactionNote ?? ''; + + Map toJson() => { + 'id': id, + 'recipientAddress': recipientAddress, + 'transactionNote': transactionNote, + }; + + factory TransactionDescription.fromJson(Map json) { + return TransactionDescription( + id: json['id'] as String, + recipientAddress: json['recipientAddress'] as String?, + transactionNote: json['transactionNote'] as String?, + ); + } } diff --git a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart index ff5513502..a86645ecb 100644 --- a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart +++ b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart @@ -1,27 +1,49 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; +import 'package:flutter/material.dart'; -class TextFieldListRow extends StatelessWidget { +class TextFieldListRow extends StatefulWidget { TextFieldListRow( {required this.title, required this.value, this.titleFontSize = 14, this.valueFontSize = 16, - this.onSubmitted, - this.onTapOutside}) - : _textController = TextEditingController() { - _textController.text = value; - } + this.onSubmitted}); final String title; final String value; final double titleFontSize; final double valueFontSize; final Function(String value)? onSubmitted; - final Function(String value)? onTapOutside; - final TextEditingController _textController; + + @override + _TextFieldListRowState createState() => _TextFieldListRowState(); +} + +class _TextFieldListRowState extends State { + late TextEditingController _textController; + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _textController = TextEditingController(text: widget.value); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + widget.onSubmitted?.call(_textController.text); + } + }); + } + + @override + void dispose() { + _textController.dispose(); + _focusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -29,41 +51,48 @@ class TextFieldListRow extends StatelessWidget { width: double.infinity, color: Theme.of(context).colorScheme.background, child: Padding( - padding: - const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), + padding: const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: TextStyle( - fontSize: titleFontSize, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.detailsTitlesColor), - textAlign: TextAlign.left), - TextField( - controller: _textController, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.done, - maxLines: null, - textAlign: TextAlign.start, - style: TextStyle( - fontSize: valueFontSize, - fontWeight: FontWeight.w500, - color: - Theme.of(context).extension()!.titleColor), - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.only(top: 12, bottom: 0), - hintText: S.of(context).enter_your_note, - hintStyle: TextStyle( - fontSize: valueFontSize, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.detailsTitlesColor), - border: InputBorder.none), - onTapOutside: (_) => onTapOutside?.call(_textController.text), - onSubmitted: (value) => onSubmitted?.call(value), - ) - ]), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: TextStyle( + fontSize: widget.titleFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.detailsTitlesColor, + ), + textAlign: TextAlign.left, + ), + TextField( + controller: _textController, + focusNode: _focusNode, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.done, + maxLines: null, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: widget.valueFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.only(top: 12, bottom: 0), + hintText: S.of(context).enter_your_note, + hintStyle: TextStyle( + fontSize: widget.valueFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.detailsTitlesColor, + ), + border: InputBorder.none, + ), + onSubmitted: (value) { + widget.onSubmitted?.call(value); + }, + ), + ], + ), ), ); } diff --git a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart index 61689b52a..165995ba0 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart @@ -44,7 +44,6 @@ class UnspentCoinsDetailsPage extends BasePage { return TextFieldListRow( title: item.title, value: item.value, - onTapOutside: item.onSubmitted, onSubmitted: item.onSubmitted, ); } From cc61a25cd17f8662f56672e4d19f0a43579057f3 Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 4 Oct 2024 23:53:55 +0200 Subject: [PATCH 114/142] Set flush: true in backup service (#1717) This should ensure that files are flushed to disk before this function returns. Also, replaced foreach loop with for loop. --- lib/core/backup_service.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 26e2136dc..ba90a0449 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -130,18 +130,18 @@ class BackupService { final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); - zip.files.forEach((file) { + for (var file in zip.files) { final filename = file.name; if (file.isFile) { final content = file.content as List; File('${appDir.path}/' + filename) ..createSync(recursive: true) - ..writeAsBytesSync(content); + ..writeAsBytesSync(content, flush: true); } else { Directory('${appDir.path}/' + filename)..create(recursive: true); } - }); + }; await _verifyWallets(); await _importKeychainDumpV1(password, nonce: nonce); @@ -153,18 +153,18 @@ class BackupService { final decryptedData = await _decryptV2(data, password); final zip = ZipDecoder().decodeBytes(decryptedData); - zip.files.forEach((file) { + for (var file in zip.files) { final filename = file.name; if (file.isFile) { final content = file.content as List; File('${appDir.path}/' + filename) ..createSync(recursive: true) - ..writeAsBytesSync(content); + ..writeAsBytesSync(content, flush: true); } else { Directory('${appDir.path}/' + filename)..create(recursive: true); } - }); + }; await _verifyWallets(); await _importKeychainDumpV2(password); From 382a0ff35ddb21b70aa1cde33cb32f258e85d02e Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 5 Oct 2024 02:30:16 +0200 Subject: [PATCH 115/142] Various monero fixes (#1722) * enable autogenerate subaddress on xmr in different place fix monero_com build fix autogenerate accounts in address book * Show account number for all wallets * generate addressbook with latest addresses regardless of isAutoGenerateEnabled --- lib/reactions/on_current_wallet_change.dart | 5 --- .../contact_list/contact_list_view_model.dart | 36 +++++++++---------- tool/configure.dart | 1 + 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 6f1ba1d8c..6630f1dfc 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -152,11 +152,6 @@ void _setAutoGenerateSubaddressStatus( WalletBase, TransactionInfo> wallet, SettingsStore settingsStore, ) async { - final walletHasAddresses = await wallet.walletAddresses.addressesMap.length > 1; - if (settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized && - walletHasAddresses) { - settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled; - } wallet.isEnabledAutoGenerateSubaddress = settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled || settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized; diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index df6cbdb9f..4089d988b 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -26,22 +26,23 @@ abstract class ContactListViewModelBase with Store { isAutoGenerateEnabled = settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { - if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) { - final key = info.addressInfos!.keys.first; - final value = info.addressInfos![key]; - final address = value?.first; - if (address != null) { - final name = _createName(info.name, address.label); - walletContacts.add(WalletContact( - address.address, - name, - walletTypeToCryptoCurrency(info.type), - )); + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) && info.addressInfos != null) { + for (var key in info.addressInfos!.keys) { + final value = info.addressInfos![key]; + final address = value?.first; + if (address != null) { + final name = _createName(info.name, address.label, key: key); + walletContacts.add(WalletContact( + address.address, + name, + walletTypeToCryptoCurrency(info.type), + )); + } } } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) { final address = info.address; - final name = _createName(info.name, ""); + final name = _createName(info.name, "", key: 0); walletContacts.add(WalletContact( address, name, @@ -52,7 +53,7 @@ abstract class ContactListViewModelBase with Store { if (label.isEmpty) { return; } - final name = _createName(info.name, label); + final name = _createName(info.name, label, key: null); walletContacts.add(WalletContact( address, name, @@ -65,7 +66,7 @@ abstract class ContactListViewModelBase with Store { } else { walletContacts.add(WalletContact( info.address, - info.name, + _createName(info.name, "", key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) ? 0 : null), walletTypeToCryptoCurrency(info.type), )); } @@ -76,10 +77,9 @@ abstract class ContactListViewModelBase with Store { initialFire: true); } - String _createName(String walletName, String label) { - return label.isNotEmpty - ? '$walletName (${label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments)})' - : walletName; + String _createName(String walletName, String label, {int? key = null}) { + final actualLabel = label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments); + return '$walletName${key == null ? "" : " [#${key}]"} ${actualLabel.isNotEmpty ? "($actualLabel)" : ""}'.trim(); } final bool isAutoGenerateEnabled; diff --git a/tool/configure.dart b/tool/configure.dart index c3d84c4f6..0c0f491a5 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -84,6 +84,7 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/receive_page_option.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; From 37b822b7f522736779839fa00e36b12004f3d502 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Fri, 4 Oct 2024 17:30:52 -0700 Subject: [PATCH 116/142] Mweb enhancements (#1715) * node peer enhancement, delay mweb address generation, increase logging * prevent unnecessary sync status changes if we can't connect to the ltc node * handle potential errors * set nodeUri to null for testing * [skip ci] redo good changes * [skip ci] draft * [skip ci] minor * [skip ci] cleanup * [skip ci] minor * [skip ci] minor * [skip ci] localization * [skip ci] save * [skip ci] wip * use proxy layer * ui * minor changes Add ToDos for later * fixes * [skip ci] minor * [skip ci] minor * [skip ci] ui * handle case where there are no addresses with txcount > 0 * comment out pegin button --------- Co-authored-by: OmarHatem --- cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 +- cw_bitcoin/lib/litecoin_wallet.dart | 94 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 30 +- cw_core/lib/sync_status.dart | 10 +- .../com/cakewallet/mweb/CwMwebPlugin.kt | 3 +- cw_mweb/ios/Classes/CwMwebPlugin.swift | 5 +- cw_mweb/lib/cw_mweb.dart | 3 +- cw_mweb/lib/cw_mweb_method_channel.dart | 5 +- cw_mweb/lib/cw_mweb_platform_interface.dart | 2 +- lib/bitcoin/cw_bitcoin.dart | 11 + lib/core/sync_status_title.dart | 11 +- .../screens/dashboard/pages/balance_page.dart | 839 +++++++++++------- .../dashboard/widgets/menu_widget.dart | 2 +- lib/src/widgets/setting_actions.dart | 2 +- .../dashboard/balance_view_model.dart | 5 +- .../dashboard/dashboard_view_model.dart | 23 +- .../wallet_address_list_view_model.dart | 51 +- res/values/strings_ar.arb | 6 + res/values/strings_bg.arb | 6 + res/values/strings_cs.arb | 6 + res/values/strings_de.arb | 6 + res/values/strings_en.arb | 10 +- res/values/strings_es.arb | 6 + res/values/strings_fr.arb | 6 + res/values/strings_ha.arb | 6 + res/values/strings_hi.arb | 6 + res/values/strings_hr.arb | 6 + res/values/strings_hy.arb | 6 + res/values/strings_id.arb | 6 + res/values/strings_it.arb | 6 + res/values/strings_ja.arb | 6 + res/values/strings_ko.arb | 6 + res/values/strings_my.arb | 6 + res/values/strings_nl.arb | 6 + res/values/strings_pl.arb | 6 + res/values/strings_pt.arb | 6 + res/values/strings_ru.arb | 6 + res/values/strings_th.arb | 6 + res/values/strings_tl.arb | 6 + res/values/strings_tr.arb | 6 + res/values/strings_uk.arb | 6 + res/values/strings_ur.arb | 6 + res/values/strings_vi.arb | 6 + res/values/strings_yo.arb | 6 + res/values/strings_zh.arb | 6 + tool/configure.dart | 2 + 46 files changed, 853 insertions(+), 421 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 61d4fdbc3..91345d726 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -100,10 +100,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const gap = 20; final ObservableList _addresses; - late ObservableList addressesByReceiveType; + final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; + // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList silentAddresses; + // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList mwebAddresses; final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 mainHd; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index d7e6fef61..8fe02ecd3 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -236,16 +236,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Future waitForMwebAddresses() async { // ensure that we have the full 1000 mweb addresses generated before continuing: // should no longer be needed, but leaving here just in case - final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; - while (mwebAddrs.length < 1000) { - print("waiting for mweb addresses to finish generating..."); - await Future.delayed(const Duration(milliseconds: 1000)); - } + // final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + // while (mwebAddrs.length < 1000) { + // print("waiting for mweb addresses to finish generating..."); + // await Future.delayed(const Duration(milliseconds: 1000)); + // } + await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020); } @action @override Future startSync() async { + print("startSync() called!"); if (syncStatus is SyncronizingSyncStatus) { return; } @@ -289,46 +291,59 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { if (syncStatus is FailedSyncStatus) return; + print("SYNCING...."); + final nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node - final resp = await CwMweb.status(StatusRequest()); - print("resp.mwebUtxosHeight: ${resp.mwebUtxosHeight}"); - print("resp.mwebHeaderHeight: ${resp.mwebHeaderHeight}"); - print("resp.blockHeaderHeight: ${resp.blockHeaderHeight}"); - if (resp.blockHeaderHeight < nodeHeight) { - int h = resp.blockHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); - } else if (resp.mwebHeaderHeight < nodeHeight) { - int h = resp.mwebHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); - } else if (resp.mwebUtxosHeight < nodeHeight) { - syncStatus = SyncingSyncStatus(1, 0.999); - } else { - if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { - await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); - await checkMwebUtxosSpent(); - // update the confirmations for each transaction: - for (final transaction in transactionHistory.transactions.values) { - if (transaction.isPending) continue; - int txHeight = transaction.height ?? resp.mwebUtxosHeight; - final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; - if (transaction.confirmations == confirmations) continue; - transaction.confirmations = confirmations; - transactionHistory.addOne(transaction); - } - await transactionHistory.save(); - } - - // prevent unnecessary reaction triggers: - if (syncStatus is! SyncedSyncStatus) { - // mwebd is synced, but we could still be processing incoming utxos: - if (!processingUtxos) { - syncStatus = SyncedSyncStatus(); - } + if (nodeHeight == 0) { + // we aren't connected to the ltc node yet + if (syncStatus is! NotConnectedSyncStatus) { + syncStatus = FailedSyncStatus(error: "Failed to connect to Litecoin node"); } return; } + + final resp = await CwMweb.status(StatusRequest()); + + try { + if (resp.blockHeaderHeight < nodeHeight) { + int h = resp.blockHeaderHeight; + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebHeaderHeight < nodeHeight) { + int h = resp.mwebHeaderHeight; + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebUtxosHeight < nodeHeight) { + syncStatus = SyncingSyncStatus(1, 0.999); + } else { + if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { + await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); + await checkMwebUtxosSpent(); + // update the confirmations for each transaction: + for (final transaction in transactionHistory.transactions.values) { + if (transaction.isPending) continue; + int txHeight = transaction.height ?? resp.mwebUtxosHeight; + final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; + if (transaction.confirmations == confirmations) continue; + transaction.confirmations = confirmations; + transactionHistory.addOne(transaction); + } + await transactionHistory.save(); + } + + // prevent unnecessary reaction triggers: + if (syncStatus is! SyncedSyncStatus) { + // mwebd is synced, but we could still be processing incoming utxos: + if (!processingUtxos) { + syncStatus = SyncedSyncStatus(); + } + } + return; + } + } catch (e) { + print("error syncing: $e"); + syncStatus = FailedSyncStatus(error: e.toString()); + } }); } @@ -411,6 +426,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future handleIncoming(MwebUtxo utxo, RpcClient stub) async { + print("handleIncoming() called!"); final status = await stub.status(StatusRequest()); var date = DateTime.now(); var confirmations = 0; diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 3a7856516..88f640d85 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -70,13 +70,9 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with await Future.delayed(Duration(milliseconds: 100)); } } - } - Future initMwebAddresses() async { - if (mwebAddrs.length < 1000) { - print("Generating MWEB addresses..."); - await ensureMwebAddressUpToIndexExists(1020); - print("done generating MWEB addresses"); + // ensure mweb addresses are up to date: + if (mwebAddresses.length < mwebAddrs.length) { List addressRecords = mwebAddrs .asMap() .entries @@ -88,7 +84,27 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with )) .toList(); addMwebAddresses(addressRecords); - print("added ${addressRecords.length} mweb addresses"); + print("set ${addressRecords.length} mweb addresses"); + } + } + + Future initMwebAddresses() async { + if (mwebAddrs.length < 1000) { + print("Generating MWEB addresses..."); + await ensureMwebAddressUpToIndexExists(20); + print("done generating MWEB addresses"); + // List addressRecords = mwebAddrs + // .asMap() + // .entries + // .map((e) => BitcoinAddressRecord( + // e.value, + // index: e.key, + // type: SegwitAddresType.mweb, + // network: network, + // )) + // .toList(); + // addMwebAddresses(addressRecords); + // print("added ${addressRecords.length} mweb addresses"); return; } } diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 627b513b2..7d6b0a285 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -67,7 +67,13 @@ class AttemptingScanSyncStatus extends SyncStatus { double progress() => 0.0; } -class FailedSyncStatus extends NotConnectedSyncStatus {} +class FailedSyncStatus extends NotConnectedSyncStatus { + String? error; + FailedSyncStatus({this.error}); + + @override + String toString() => error ?? super.toString(); +} class ConnectingSyncStatus extends SyncStatus { @override @@ -89,4 +95,4 @@ class TimedOutSyncStatus extends NotConnectedSyncStatus { class LostConnectionSyncStatus extends NotConnectedSyncStatus { @override String toString() => 'Reconnecting'; -} \ No newline at end of file +} diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt index 57ae3d4c3..0cc4cc537 100644 --- a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -30,7 +30,8 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { if (call.method == "start") { server?.stop() val dataDir = call.argument("dataDir") ?: "" - server = server ?: Mwebd.newServer("", dataDir, "") + val nodeUri = call.argument("nodeUri") ?: "" + server = server ?: Mwebd.newServer("", dataDir, nodeUri) port = server?.start(0) result.success(port) } else if (call.method == "stop") { diff --git a/cw_mweb/ios/Classes/CwMwebPlugin.swift b/cw_mweb/ios/Classes/CwMwebPlugin.swift index f1fd78cd8..92061ded8 100644 --- a/cw_mweb/ios/Classes/CwMwebPlugin.swift +++ b/cw_mweb/ios/Classes/CwMwebPlugin.swift @@ -12,6 +12,7 @@ public static func register(with registrar: FlutterPluginRegistrar) { private static var server: MwebdServer? private static var port: Int = 0 private static var dataDir: String? + private static var nodeUri: String? public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { @@ -22,7 +23,9 @@ public static func register(with registrar: FlutterPluginRegistrar) { stopServer() let args = call.arguments as? [String: String] let dataDir = args?["dataDir"] + let nodeUri = args?["nodeUri"] CwMwebPlugin.dataDir = dataDir + CwMwebPlugin.nodeUri = nodeUri startServer(result: result) break case "stop": @@ -48,7 +51,7 @@ public static func register(with registrar: FlutterPluginRegistrar) { private func startServer(result: @escaping FlutterResult) { if CwMwebPlugin.server == nil { var error: NSError? - CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, "", &error) + CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, CwMwebPlugin.nodeUri, &error) if let server = CwMwebPlugin.server { do { diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index 63ff1bf97..3be8e9bf5 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -17,7 +17,8 @@ class CwMweb { await Future.delayed(const Duration(seconds: 5)); final appDir = await getApplicationSupportDirectory(); - _port = await CwMwebPlatform.instance.start(appDir.path); + const ltcNodeUri = "45.79.13.180:9333"; + _port = await CwMwebPlatform.instance.start(appDir.path, ltcNodeUri); if (_port == null || _port == 0) { throw Exception("Failed to start server"); } diff --git a/cw_mweb/lib/cw_mweb_method_channel.dart b/cw_mweb/lib/cw_mweb_method_channel.dart index 70e4a1789..4956dcb8d 100644 --- a/cw_mweb/lib/cw_mweb_method_channel.dart +++ b/cw_mweb/lib/cw_mweb_method_channel.dart @@ -10,8 +10,9 @@ class MethodChannelCwMweb extends CwMwebPlatform { final methodChannel = const MethodChannel('cw_mweb'); @override - Future start(String dataDir) async { - final result = await methodChannel.invokeMethod('start', {'dataDir': dataDir}); + Future start(String dataDir, String nodeUri) async { + final result = + await methodChannel.invokeMethod('start', {'dataDir': dataDir, 'nodeUri': nodeUri}); return result; } diff --git a/cw_mweb/lib/cw_mweb_platform_interface.dart b/cw_mweb/lib/cw_mweb_platform_interface.dart index 8cc80f3e9..d5c03a56c 100644 --- a/cw_mweb/lib/cw_mweb_platform_interface.dart +++ b/cw_mweb/lib/cw_mweb_platform_interface.dart @@ -25,7 +25,7 @@ abstract class CwMwebPlatform extends PlatformInterface { _instance = instance; } - Future start(String dataDir) { + Future start(String dataDir, String nodeUri) { throw UnimplementedError('start() has not been implemented.'); } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 50c22d113..2bed8a451 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -654,4 +654,15 @@ class CWBitcoin extends Bitcoin { // TODO: this could be improved: return inputAddressesContainMweb || outputAddressesContainMweb; } + + String? getUnusedMwebAddress(Object wallet) { + try { + final electrumWallet = wallet as ElectrumWallet; + final walletAddresses = electrumWallet.walletAddresses as ElectrumWalletAddresses; + final mwebAddress = walletAddresses.mwebAddresses.firstWhere((element) => !element.isUsed); + return mwebAddress.address; + } catch (_) { + return null; + } + } } diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 4582f7b1f..46dd62c3a 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -16,6 +16,13 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_syncronized; } + if (syncStatus is FailedSyncStatus) { + if (syncStatus.error != null) { + return syncStatus.error!; + } + return S.current.sync_status_failed_connect; + } + if (syncStatus is NotConnectedSyncStatus) { return S.current.sync_status_not_connected; } @@ -24,10 +31,6 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_attempting_sync; } - if (syncStatus is FailedSyncStatus) { - return S.current.sync_status_failed_connect; - } - if (syncStatus is ConnectingSyncStatus) { return S.current.sync_status_connecting; } diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index a7b2a99fa..08e045a0d 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; @@ -19,14 +20,14 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; import 'package:url_launcher/url_launcher.dart'; class BalancePage extends StatelessWidget { @@ -238,8 +239,8 @@ class CryptoBalanceWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DashBoardRoundedCardWidget( - title: S.current.rep_warning, - subTitle: S.current.rep_warning_sub, + title: S.of(context).rep_warning, + subTitle: S.of(context).rep_warning_sub, onTap: () => Navigator.of(context).pushNamed(Routes.changeRep), onClose: () { dashboardViewModel.settingsStore.shouldShowRepWarning = false; @@ -259,6 +260,7 @@ class CryptoBalanceWidget extends StatelessWidget { dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); return Observer(builder: (_) { return BalanceRowWidget( + dashboardViewModel: dashboardViewModel, availableBalanceLabel: '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', availableBalance: balance.availableBalance, @@ -379,57 +381,68 @@ class CryptoBalanceWidget extends StatelessWidget { padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DashBoardRoundedCardWidget( customBorder: 30, - title: S.current.litecoin_mweb, - subTitle: S.current.litecoin_enable_mweb_sync, + title: S.of(context).litecoin_mweb, + subTitle: '', hint: Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), - mode: LaunchMode.externalApplication, + Text( + S.of(context).litecoin_mweb_description, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 8), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, + ), + child: Center( + child: Text( + S.of(context).learn_more, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, ), - child: Row( - children: [ - Text( - S.current.litecoin_what_is_mweb, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - softWrap: true, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ) - ], + softWrap: true, + ), + ), + ), + SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => _dismissMweb(context), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: Text( + S.of(context).litecoin_mweb_dismiss, + style: TextStyle(color: Colors.white), ), ), - Observer( - builder: (_) => StandardSwitch( - value: dashboardViewModel.mwebScanningActive, - onTaped: () => _toggleMweb(context), - ), - ) + ElevatedButton( + onPressed: () => _enableMweb(context), + child: Text(S.of(context).litecoin_enable_mweb_sync), + ), ], ), ], ), - onTap: () => _toggleMweb(context), + onTap: () => {}, icon: ImageIcon( AssetImage('assets/images/mweb_logo.png'), color: @@ -479,20 +492,34 @@ class CryptoBalanceWidget extends StatelessWidget { return dashboardViewModel.setSilentPaymentsScanning(newValue); } - Future _toggleMweb(BuildContext context) async { + Future _enableMweb(BuildContext context) async { if (!dashboardViewModel.hasEnabledMwebBefore) { await showPopUp( context: context, builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).warning, - alertContent: S.current.litecoin_mweb_warning, - buttonText: S.of(context).ok, + alertTitle: S.of(context).alert_notice, + alertContent: S.of(context).litecoin_mweb_warning, + buttonText: S.of(context).understand, buttonAction: () { Navigator.of(context).pop(); }, )); } - dashboardViewModel.setMwebScanningActive(!dashboardViewModel.mwebScanningActive); + dashboardViewModel.setMwebScanningActive(); + } + + Future _dismissMweb(BuildContext context) async { + await showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).alert_notice, + alertContent: S.of(context).litecoin_mweb_enable_later, + buttonText: S.of(context).understand, + buttonAction: () { + Navigator.of(context).pop(); + }, + )); + dashboardViewModel.dismissMweb(); } } @@ -517,6 +544,7 @@ class BalanceRowWidget extends StatelessWidget { required this.hasSecondAvailableBalance, required this.hasSecondAdditionalBalance, required this.isTestnet, + required this.dashboardViewModel, super.key, }); @@ -539,187 +567,238 @@ class BalanceRowWidget extends StatelessWidget { final bool hasSecondAvailableBalance; final bool hasSecondAdditionalBalance; final bool isTestnet; + final DashboardViewModel dashboardViewModel; // void _showBalanceDescription(BuildContext context) { // showPopUp( // context: context, // builder: (_) => - // InformationPage(information: S.current.available_balance_description), + // InformationPage(information: S.of(context).available_balance_description), // ); // } @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(left: 16, right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - width: 1, + return Column(children: [ + Container( + margin: const EdgeInsets.only(left: 16, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, + ), + color: Theme.of(context).extension()!.syncedBackgroundColor, ), - color: Theme.of(context).extension()!.syncedBackgroundColor, - ), - child: Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ + child: Container( + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: hasAdditionalBalance + ? () => _showBalanceDescription( + context, S.of(context).available_balance_description) + : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Semantics( + hint: 'Double tap to see more information', + container: true, + child: Text('${availableBalanceLabel}', + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1)), + ), + if (hasAdditionalBalance) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ), + ], + ), + SizedBox(height: 6), + AutoSizeText(availableBalance, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + color: Theme.of(context) + .extension()! + .balanceAmountColor, + height: 1), + maxLines: 1, + textAlign: TextAlign.start), + SizedBox(height: 6), + if (isTestnet) + Text(S.of(context).testnet_coins_no_value, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1)), + if (!isTestnet) + Text('${availableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor, + height: 1)), + ], + ), + ), + SizedBox( + width: min(MediaQuery.of(context).size.width * 0.2, 100), + child: Center( + child: Column( + children: [ + CakeImageWidget( + imageUrl: currency.iconPath, + height: 40, + width: 40, + displayOnError: Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title.substring(0, min(currency.title.length, 2)), + style: TextStyle(fontSize: 11), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), + ), + ), + const SizedBox(height: 10), + Text( + currency.title, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: + Theme.of(context).extension()!.assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ), + ], + ), + if (frozenBalance.isNotEmpty) GestureDetector( behavior: HitTestBehavior.opaque, onTap: hasAdditionalBalance - ? () => - _showBalanceDescription(context, S.current.available_balance_description) + ? () => _showBalanceDescription( + context, S.of(context).unavailable_balance_description) : null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 26), Row( children: [ - Semantics( - hint: 'Double tap to see more information', - container: true, - child: Text('${availableBalanceLabel}', - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1)), - ), - if (hasAdditionalBalance) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), + Text( + S.of(context).unavailable_balance, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.labelTextColor, + height: 1, ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ), ], ), - SizedBox(height: 6), - AutoSizeText(availableBalance, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w900, - color: Theme.of(context) - .extension()! - .balanceAmountColor, - height: 1), - maxLines: 1, - textAlign: TextAlign.start), - SizedBox(height: 6), - if (isTestnet) - Text(S.current.testnet_coins_no_value, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1)), + SizedBox(height: 8), + AutoSizeText( + frozenBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.balanceAmountColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), if (!isTestnet) - Text('${availableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor, - height: 1)), - ], - ), - ), - SizedBox( - width: min(MediaQuery.of(context).size.width * 0.2, 100), - child: Center( - child: Column( - children: [ - CakeImageWidget( - imageUrl: currency.iconPath, - height: 40, - width: 40, - displayOnError: Container( - height: 30.0, - width: 30.0, - child: Center( - child: Text( - currency.title.substring(0, min(currency.title.length, 2)), - style: TextStyle(fontSize: 11), - ), - ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey.shade400, - ), - ), - ), - const SizedBox(height: 10), Text( - currency.title, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - ), - ], - ), - ), - ), - ], - ), - if (frozenBalance.isNotEmpty) - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: hasAdditionalBalance - ? () => - _showBalanceDescription(context, S.current.unavailable_balance_description) - : null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 26), - Row( - children: [ - Text( - S.current.unavailable_balance, + frozenFiatBalance, textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, + color: Theme.of(context).extension()!.textColor, height: 1, ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: - Theme.of(context).extension()!.labelTextColor), - ), - ], + ], + ), + ), + if (hasAdditionalBalance) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${additionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), ), SizedBox(height: 8), AutoSizeText( - frozenBalance, + additionalBalance, style: TextStyle( fontSize: 20, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.balanceAmountColor, + color: Theme.of(context).extension()!.assetTitleColor, height: 1, ), maxLines: 1, @@ -728,7 +807,7 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(height: 4), if (!isTestnet) Text( - frozenFiatBalance, + '${additionalFiatBalance}', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, @@ -740,143 +819,247 @@ class BalanceRowWidget extends StatelessWidget { ), ], ), - ), - if (hasAdditionalBalance) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${additionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - additionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${additionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - if (hasSecondAvailableBalance) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAvailableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAvailableBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${secondAvailableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - if (hasSecondAdditionalBalance) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAdditionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAdditionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${secondAdditionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - ], + ], + ), ), ), - ); + if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[ + SizedBox(height: 16), + Container( + margin: const EdgeInsets.only(left: 16, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, + ), + color: Theme.of(context).extension()!.syncedBackgroundColor, + ), + child: Container( + margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + if (currency == CryptoCurrency.ltc) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: EdgeInsets.only(right: 16, top: 16), + child: Column( + children: [ + CakeImageWidget( + imageUrl: 'assets/images/mweb_logo.png', + height: 40, + width: 40, + displayOnError: Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title.substring(0, min(currency.title.length, 2)), + style: TextStyle(fontSize: 11), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), + ), + ), + const SizedBox(height: 10), + Text( + 'MWEB', + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ], + ), + if (hasSecondAvailableBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAvailableBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAvailableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), + ], + ), + ], + ), + Stack( + children: [ + if (hasSecondAdditionalBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAdditionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAdditionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAdditionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), + ], + ), + // TODO: smarter peg in / out buttons + // if (currency == CryptoCurrency.ltc) + // Row( + // mainAxisAlignment: MainAxisAlignment.end, + // children: [ + // Container( + // margin: EdgeInsets.only(top: 24, right: 8), + // child: ElevatedButton( + // style: ElevatedButton.styleFrom( + // backgroundColor: Theme.of(context).highlightColor, + // ), + // onPressed: () { + // final mwebAddress = + // bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); + // if (mwebAddress == null) return; + // final paymentRequest = + // PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); + // Navigator.of(context) + // .pushNamed(Routes.send, arguments: paymentRequest); + // }, + // child: Container( + // color: Colors.transparent, + // margin: EdgeInsets.all(4), + // child: Column( + // mainAxisSize: MainAxisSize.max, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // Container( + // alignment: Alignment.center, + // decoration: BoxDecoration(shape: BoxShape.circle), + // child: Image.asset( + // 'assets/images/received.png', + // color: Theme.of(context) + // .extension()! + // .balanceAmountColor, + // width: 64, + // height: 32, + // ), + // ), + // SizedBox(height: 4), + // Text( + // S.of(context).litecoin_mweb_pegin, + // style: TextStyle( + // fontSize: 10, + // color: Theme.of(context) + // .extension()! + // .cardTextColor), + // ) + // ], + // ), + // ), + // ), + // ), + // ], + // ), + ], + ), + ], + ), + ), + ), + ], + ]); } void _showBalanceDescription(BuildContext context, String content) { diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 30c7b7f78..a59bfd2fa 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -192,7 +192,7 @@ class MenuWidgetState extends State { final item = items[index]; if (!widget.dashboardViewModel.hasMweb && - item.name(context) == S.current.litecoin_mweb_settings) { + item.name(context) == S.of(context).litecoin_mweb_settings) { return const SizedBox(); } diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index a8a9558d5..80d07962f 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -49,7 +49,7 @@ class SettingActions { ); static SettingActions litecoinMwebSettingAction = SettingActions._( - name: (context) => S.current.litecoin_mweb_settings, + name: (context) => S.of(context).litecoin_mweb_settings, image: 'assets/images/bitcoin_menu.png', onTap: (BuildContext context) { Navigator.pop(context); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index c3fb5718a..083c31302 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -358,14 +358,15 @@ abstract class BalanceViewModelBase with Store { } bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) { + if (wallet.type == WalletType.litecoin && settingsStore.mwebAlwaysScan) { + // if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) return true; } return false; } bool _hasSecondAvailableBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) { + if (wallet.type == WalletType.litecoin && settingsStore.mwebAlwaysScan) { return true; } return false; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 21a167e2a..77c60bd63 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -262,6 +262,8 @@ abstract class DashboardViewModelBase with Store { reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) { if (alwaysScan) { mwebScanningActive = true; + } else { + mwebScanningActive = false; } }); } @@ -431,7 +433,7 @@ abstract class DashboardViewModelBase with Store { bool get hasMweb => wallet.type == WalletType.litecoin; @computed - bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay; + bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebScanningActive; @observable bool mwebScanningActive = false; @@ -440,18 +442,23 @@ abstract class DashboardViewModelBase with Store { bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; @action - void setMwebScanningActive(bool active) { + void setMwebScanningActive() { if (!hasMweb) { return; } - if (active) { - settingsStore.hasEnabledMwebBefore = true; - } + settingsStore.hasEnabledMwebBefore = true; + mwebScanningActive = true; + bitcoin!.setMwebEnabled(wallet, true); + settingsStore.mwebAlwaysScan = true; + } - settingsStore.mwebEnabled = active; - mwebScanningActive = active; - bitcoin!.setMwebEnabled(wallet, active); + @action + void dismissMweb() { + settingsStore.mwebCardDisplay = false; + settingsStore.mwebAlwaysScan = false; + mwebScanningActive = false; + bitcoin!.setMwebEnabled(wallet, false); } BalanceViewModel balanceViewModel; diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 9fb7509eb..0260dc6b4 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -326,13 +326,13 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( - id: subaddress.id, - isPrimary: isPrimary, - name: subaddress.label, - address: subaddress.address, - balance: subaddress.received, - txCount: subaddress.txCount, - ); + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.label, + address: subaddress.address, + balance: subaddress.received, + txCount: subaddress.txCount, + ); }); addressList.addAll(addressItems); } @@ -418,8 +418,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type == WalletType.litecoin && addressItems.length >= 1000) { // find the index of the last item with a txCount > 0 final addressItemsList = addressItems.toList(); - final lastItemWithTxCount = addressItemsList.lastWhere((item) => (item.txCount ?? 0) > 0); - final index = addressItemsList.indexOf(lastItemWithTxCount); + int index = addressItemsList.lastIndexWhere((item) => (item.txCount ?? 0) > 0); + if (index == -1) { + index = 0; + } // show only up to that index + 20: addressItems = addressItemsList.sublist(0, index + 20); } @@ -461,14 +463,16 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; - (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses.contains((addressList[i] as WalletAddressListItem).address); + (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses + .contains((addressList[i] as WalletAddressListItem).address); } for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; - (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses.contains((addressList[i] as WalletAddressListItem).address); + (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses + .contains((addressList[i] as WalletAddressListItem).address); } - + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { @@ -480,6 +484,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return addressList; } + Future toggleHideAddress(WalletAddressListItem item) async { if (item.isHidden) { wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address); @@ -488,13 +493,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } await wallet.walletAddresses.saveAddressesInBox(); if (wallet.type == WalletType.monero) { - monero!.getSubaddressList(wallet).update(wallet, accountIndex: monero!.getCurrentAccount(wallet).id); + monero! + .getSubaddressList(wallet) + .update(wallet, accountIndex: monero!.getCurrentAccount(wallet).id); } else if (wallet.type == WalletType.wownero) { - wownero!.getSubaddressList(wallet).update(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id); + wownero! + .getSubaddressList(wallet) + .update(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id); } else if (wallet.type == WalletType.haven) { - haven!.getSubaddressList(wallet).update(wallet, accountIndex: haven!.getCurrentAccount(wallet).id); + haven! + .getSubaddressList(wallet) + .update(wallet, accountIndex: haven!.getCurrentAccount(wallet).id); } } + @observable bool hasAccounts; @@ -535,8 +547,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed bool get isReceivedAvailable => - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero; + wallet.type == WalletType.monero || wallet.type == WalletType.wownero; @computed bool get isSilentPayments => @@ -549,9 +560,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed bool get showAddManualAddresses => - !isAutoGenerateSubaddressEnabled || - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero; + !isAutoGenerateSubaddressEnabled || + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; List _baseItems; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index e81696b2a..d55faad76 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -36,6 +36,7 @@ "agree": "موافق", "agree_and_continue": "الموافقة ومتابعة", "agree_to": "من خلال إنشاء حساب فإنك توافق على", + "alert_notice": "يلاحظ", "all": "الكل", "all_trades": "جميع عمليات التداول", "all_transactions": "كل التحركات المالية", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "تمكين MWEB المسح الضوئي", "litecoin_mweb": "mweb", "litecoin_mweb_always_scan": "اضبط MWEB دائمًا على المسح الضوئي", + "litecoin_mweb_description": "MWEB هو بروتوكول جديد يجلب معاملات أسرع وأرخص وأكثر خصوصية إلى Litecoin", + "litecoin_mweb_dismiss": "رفض", "litecoin_mweb_display_card": "عرض بطاقة mweb", + "litecoin_mweb_enable_later": "يمكنك اختيار تمكين MWEB مرة أخرى ضمن إعدادات العرض.", + "litecoin_mweb_pegin": "ربط في", + "litecoin_mweb_pegout": "ربط", "litecoin_mweb_scanning": "MWEB المسح الضوئي", "litecoin_mweb_settings": "إعدادات MWEB", "litecoin_mweb_warning": "سيقوم استخدام MWEB في البداية بتنزيل ~ 600 ميجابايت من البيانات ، وقد يستغرق ما يصل إلى 30 دقيقة حسب سرعة الشبكة. سيتم تنزيل هذه البيانات الأولية مرة واحدة فقط وستكون متاحة لجميع محافظ Litecoin", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 1ddcf99f9..dd5c9a260 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -36,6 +36,7 @@ "agree": "Съгласен/а съм", "agree_and_continue": "Съгласяване и продължаване", "agree_to": "Чрез създаването на акаунт вие се съгласявате с ", + "alert_notice": "Забележете", "all": "ALL", "all_trades": "Всички сделкки", "all_transactions": "Всички транзакции", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Активирайте сканирането на MWeb", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Задайте MWeb винаги сканиране", + "litecoin_mweb_description": "MWeb е нов протокол, който носи по -бърз, по -евтин и повече частни транзакции на Litecoin", + "litecoin_mweb_dismiss": "Уволнение", "litecoin_mweb_display_card": "Показване на MWEB карта", + "litecoin_mweb_enable_later": "Можете да изберете да активирате MWEB отново под настройките на дисплея.", + "litecoin_mweb_pegin": "PEG в", + "litecoin_mweb_pegout": "PEG OUT", "litecoin_mweb_scanning": "Сканиране на MWEB", "litecoin_mweb_settings": "Настройки на MWEB", "litecoin_mweb_warning": "Използването на MWEB първоначално ще изтегли ~ 600MB данни и може да отнеме до 30 минути в зависимост от скоростта на мрежата. Тези първоначални данни ще изтеглят само веднъж и ще бъдат достъпни за всички портфейли Litecoin", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index f295692b1..65ef50a71 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -36,6 +36,7 @@ "agree": "Souhlasím", "agree_and_continue": "Souhlasím & pokračovat", "agree_to": "Vytvořením účtu souhlasíte s ", + "alert_notice": "Oznámení", "all": "VŠE", "all_trades": "Všechny obchody", "all_transactions": "Všechny transakce", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Povolit skenování MWeb", "litecoin_mweb": "MWeb", "litecoin_mweb_always_scan": "Nastavit MWeb vždy skenování", + "litecoin_mweb_description": "MWEB je nový protokol, který do Litecoin přináší rychlejší, levnější a více soukromých transakcí", + "litecoin_mweb_dismiss": "Propustit", "litecoin_mweb_display_card": "Zobrazit kartu MWeb", + "litecoin_mweb_enable_later": "V nastavení zobrazení můžete vybrat znovu povolit MWeb.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Zkrachovat", "litecoin_mweb_scanning": "Skenování mWeb", "litecoin_mweb_settings": "Nastavení mWeb", "litecoin_mweb_warning": "Pomocí MWeb zpočátku stahuje ~ 600 MB dat a může trvat až 30 minut v závislosti na rychlosti sítě. Tato počáteční data si stáhnou pouze jednou a budou k dispozici pro všechny litecoinové peněženky", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index acb5aa919..194b1d05b 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -36,6 +36,7 @@ "agree": "stimme zu", "agree_and_continue": "Zustimmen & fortfahren", "agree_to": "Indem Sie ein Konto erstellen, stimmen Sie den ", + "alert_notice": "Beachten", "all": "ALLES", "all_trades": "Alle Trades", "all_transactions": "Alle Transaktionen", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB -Scannen", "litecoin_mweb": "MWeb", "litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen", + "litecoin_mweb_description": "MWEB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", + "litecoin_mweb_dismiss": "Zurückweisen", "litecoin_mweb_display_card": "MWEB -Karte anzeigen", + "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Abstecken", "litecoin_mweb_scanning": "MWEB Scanning", "litecoin_mweb_settings": "MWEB -Einstellungen", "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin -Brieftaschen verfügbar", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 19f939d19..c79719641 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -36,6 +36,7 @@ "agree": "Agree", "agree_and_continue": "Agree & Continue", "agree_to": "By creating account you agree to the ", + "alert_notice": "Notice", "all": "ALL", "all_trades": "All trades", "all_transactions": "All transactions", @@ -333,9 +334,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "help", - "hide": "Hide", "hidden_addresses": "Hidden Addresses", "hidden_balance": "Hidden Balance", + "hide": "Hide", "hide_details": "Hide Details", "high_contrast_theme": "High Contrast Theme", "home_screen_settings": "Home screen settings", @@ -368,7 +369,12 @@ "litecoin_enable_mweb_sync": "Enable MWEB scanning", "litecoin_mweb": "MWEB", "litecoin_mweb_always_scan": "Set MWEB always scanning", + "litecoin_mweb_description": "MWEB is a new protocol that brings faster, cheaper, and more private transactions to Litecoin", + "litecoin_mweb_dismiss": "Dismiss", "litecoin_mweb_display_card": "Show MWEB card", + "litecoin_mweb_enable_later": "You can choose to enable MWEB again under Display Settings.", + "litecoin_mweb_pegin": "Peg In", + "litecoin_mweb_pegout": "Peg Out", "litecoin_mweb_scanning": "MWEB Scanning", "litecoin_mweb_settings": "MWEB settings", "litecoin_mweb_warning": "Using MWEB will initially download ~600MB of data, and may take up to 30 minutes depending on network speed. This initial data will only download once and be available for all Litecoin wallets", @@ -685,8 +691,8 @@ "setup_your_debit_card": "Set up your debit card", "share": "Share", "share_address": "Share address", - "show": "Show", "shared_seed_wallet_groups": "Shared Seed Wallet Groups", + "show": "Show", "show_details": "Show Details", "show_keys": "Show seed/keys", "show_market_place": "Show Marketplace", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index d9b2db88a..ced8d9a13 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -36,6 +36,7 @@ "agree": "De acuerdo", "agree_and_continue": "Aceptar y continuar", "agree_to": "Al crear una cuenta, aceptas ", + "alert_notice": "Aviso", "all": "TODOS", "all_trades": "Todos los oficios", "all_transactions": "Todas las transacciones", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Establecer mweb siempre escaneo", + "litecoin_mweb_description": "Mweb es un nuevo protocolo que trae transacciones más rápidas, más baratas y más privadas a Litecoin", + "litecoin_mweb_dismiss": "Despedir", "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", + "litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.", + "litecoin_mweb_pegin": "Meter", + "litecoin_mweb_pegout": "Estirar la pata", "litecoin_mweb_scanning": "Escaneo mweb", "litecoin_mweb_settings": "Configuración de MWEB", "litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 907058095..1f8567b31 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -36,6 +36,7 @@ "agree": "d'accord", "agree_and_continue": "Accepter et continuer", "agree_to": "En créant un compte, vous acceptez les ", + "alert_notice": "Avis", "all": "TOUT", "all_trades": "Tous échanges", "all_transactions": "Toutes transactions", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Activer la numérisation MWEB", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Définir MWEB Score Scanning", + "litecoin_mweb_description": "MWEB est un nouveau protocole qui apporte des transactions plus rapides, moins chères et plus privées à Litecoin", + "litecoin_mweb_dismiss": "Rejeter", "litecoin_mweb_display_card": "Afficher la carte MWeb", + "litecoin_mweb_enable_later": "Vous pouvez choisir d'activer à nouveau MWEB sous Paramètres d'affichage.", + "litecoin_mweb_pegin": "Entraver", + "litecoin_mweb_pegout": "Crever", "litecoin_mweb_scanning": "Scann mweb", "litecoin_mweb_settings": "Paramètres MWEB", "litecoin_mweb_warning": "L'utilisation de MWEB téléchargera initialement ~ 600 Mo de données et peut prendre jusqu'à 30 minutes en fonction de la vitesse du réseau. Ces données initiales ne téléchargeront qu'une seule fois et seront disponibles pour tous les portefeuilles litecoin", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 28b698c30..1e5b5d8e3 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -36,6 +36,7 @@ "agree": "Yarda", "agree_and_continue": "Amincewa & Ci gaba", "agree_to": "Ta hanyar ƙirƙirar asusu kun yarda da", + "alert_notice": "Sanarwa", "all": "DUK", "all_trades": "Duk ciniki", "all_transactions": "Dukan Ma'amaloli", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Kunna binciken Mweb", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Saita Mweb koyaushe", + "litecoin_mweb_description": "Mweb shine sabon tsarin yarjejeniya da ya kawo da sauri, mai rahusa, da kuma ma'amaloli masu zaman kansu zuwa Litecoin", + "litecoin_mweb_dismiss": "Tuɓe \\ sallama", "litecoin_mweb_display_card": "Nuna katin Mweb", + "litecoin_mweb_enable_later": "Kuna iya zaɓar kunna Mweb kuma a ƙarƙashin saitunan nuni.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg fita", "litecoin_mweb_scanning": "Mweb scanning", "litecoin_mweb_settings": "Saitunan Mweb", "litecoin_mweb_warning": "Amfani da Mweb zai fara saukewa ~ 600MB na bayanai, kuma yana iya ɗaukar minti 30 dangane da saurin cibiyar sadarwa. Wannan bayanan farko zai saika saukarwa sau ɗaya kawai kuma a samu don duk wuraren shakatawa", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 8b46685a8..07d8311ef 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -36,6 +36,7 @@ "agree": "सहमत", "agree_and_continue": "सहमत और जारी रखें", "agree_to": "खाता बनाकर आप इससे सहमत होते हैं ", + "alert_notice": "सूचना", "all": "सब", "all_trades": "सभी व्यापार", "all_transactions": "सभी लेन - देन", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "MWEB स्कैनिंग सक्षम करें", "litecoin_mweb": "मावली", "litecoin_mweb_always_scan": "MWEB हमेशा स्कैनिंग सेट करें", + "litecoin_mweb_description": "MWEB एक नया प्रोटोकॉल है जो लिटकोइन के लिए तेजी से, सस्ता और अधिक निजी लेनदेन लाता है", + "litecoin_mweb_dismiss": "नकार देना", "litecoin_mweb_display_card": "MWEB कार्ड दिखाएं", + "litecoin_mweb_enable_later": "आप प्रदर्शन सेटिंग्स के तहत फिर से MWEB को सक्षम करने के लिए चुन सकते हैं।", + "litecoin_mweb_pegin": "खूंटी", + "litecoin_mweb_pegout": "मरना", "litecoin_mweb_scanning": "MWEB स्कैनिंग", "litecoin_mweb_settings": "MWEB सेटिंग्स", "litecoin_mweb_warning": "MWEB का उपयोग शुरू में ~ 600MB डेटा डाउनलोड करेगा, और नेटवर्क की गति के आधार पर 30 मिनट तक का समय लग सकता है। यह प्रारंभिक डेटा केवल एक बार डाउनलोड करेगा और सभी लिटकोइन वॉलेट के लिए उपलब्ध होगा", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index ab3f536c7..7969716de 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -36,6 +36,7 @@ "agree": "Slažem se", "agree_and_continue": "Slažem se i nastavi", "agree_to": "Stvaranjem računa pristajete na ", + "alert_notice": "Obavijest", "all": "SVE", "all_trades": "Svi obrti", "all_transactions": "Sve transakcije", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje", "litecoin_mweb": "MWeb", "litecoin_mweb_always_scan": "Postavite MWeb uvijek skeniranje", + "litecoin_mweb_description": "MWEB je novi protokol koji u Litecoin donosi brže, jeftinije i privatnije transakcije", + "litecoin_mweb_dismiss": "Odbaciti", "litecoin_mweb_display_card": "Prikaži MWeb karticu", + "litecoin_mweb_enable_later": "Možete odabrati da MWEB ponovo omogućite pod postavkama zaslona.", + "litecoin_mweb_pegin": "Uvući se", + "litecoin_mweb_pegout": "Odapeti", "litecoin_mweb_scanning": "MWEB skeniranje", "litecoin_mweb_settings": "Postavke MWEB -a", "litecoin_mweb_warning": "Korištenje MWEB -a u početku će preuzeti ~ 600MB podataka, a može potrajati do 30 minuta, ovisno o brzini mreže. Ovi početni podaci preuzet će samo jednom i biti dostupni za sve Litecoin novčanike", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 3d37a0a85..aa58edd64 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -36,6 +36,7 @@ "agree": "Համաձայն եմ", "agree_and_continue": "Համաձայն եմ և շարունակեմ", "agree_to": "Ստեղծելով հաշիվ դուք համաձայնում եք ", + "alert_notice": "Ծանուցում", "all": "Բոլորը", "all_trades": "Բոլոր գործարքները", "all_transactions": "Բոլոր գործառնությունները", @@ -363,6 +364,11 @@ "ledger_error_wrong_app": "Խնդրում ենք համոզվել, որ դուք բացել եք ճիշտ ծրագիրը ձեր Ledger-ում", "ledger_please_enable_bluetooth": "Խնդրում ենք միացնել Bluetooth-ը ձեր Ledger-ը հայտնաբերելու համար", "light_theme": "Լուսավոր", + "litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN", + "litecoin_mweb_dismiss": "Հեռացնել", + "litecoin_mweb_enable_later": "Կարող եք ընտրել Mweb- ը կրկին միացնել ցուցադրման պարամետրերը:", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Հափշտակել", "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով", "load_more": "Բեռնել ավելին", "loading_your_wallet": "Ձեր հաշվեհամարը բեռնում է", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 504c67d50..867040df5 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -36,6 +36,7 @@ "agree": "Setuju", "agree_and_continue": "Setuju & Lanjutkan", "agree_to": "Dengan membuat akun Anda setuju dengan ", + "alert_notice": "Melihat", "all": "SEMUA", "all_trades": "Semua perdagangan", "all_transactions": "Semua transaksi", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Atur mWeb selalu memindai", + "litecoin_mweb_description": "MWEB adalah protokol baru yang membawa transaksi yang lebih cepat, lebih murah, dan lebih pribadi ke Litecoin", + "litecoin_mweb_dismiss": "Membubarkan", "litecoin_mweb_display_card": "Tunjukkan kartu mWeb", + "litecoin_mweb_enable_later": "Anda dapat memilih untuk mengaktifkan MWEB lagi di bawah pengaturan tampilan.", + "litecoin_mweb_pegin": "Pasak masuk", + "litecoin_mweb_pegout": "Mati", "litecoin_mweb_scanning": "Pemindaian MWEB", "litecoin_mweb_settings": "Pengaturan MWEB", "litecoin_mweb_warning": "Menggunakan MWEB pada awalnya akan mengunduh ~ 600MB data, dan dapat memakan waktu hingga 30 menit tergantung pada kecepatan jaringan. Data awal ini hanya akan mengunduh sekali dan tersedia untuk semua dompet litecoin", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 668346dfa..3b20d0410 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -36,6 +36,7 @@ "agree": "d'accordo", "agree_and_continue": "Accetta e continua", "agree_to": "Creando un account accetti il ​​", + "alert_notice": "Avviso", "all": "TUTTO", "all_trades": "Svi obrti", "all_transactions": "Sve transakcije", @@ -367,7 +368,12 @@ "litecoin_enable_mweb_sync": "Abilita la scansione MWeb", "litecoin_mweb": "MWeb", "litecoin_mweb_always_scan": "Imposta MWeb per scansionare sempre", + "litecoin_mweb_description": "MWeb è un nuovo protocollo che porta transazioni più veloci, più economiche e più private a Litecoin", + "litecoin_mweb_dismiss": "Congedare", "litecoin_mweb_display_card": "Mostra la scheda MWeb", + "litecoin_mweb_enable_later": "È possibile scegliere di abilitare nuovamente MWeb nelle impostazioni di visualizzazione.", + "litecoin_mweb_pegin": "Piolo in", + "litecoin_mweb_pegout": "PEG OUT", "litecoin_mweb_scanning": "Scansione MWeb", "litecoin_mweb_settings": "Impostazioni MWeb", "litecoin_mweb_warning": "L'uso di MWeb inizialmente scaricherà ~ 600 MB di dati e potrebbe richiedere fino a 30 minuti a seconda della velocità di rete. Questi dati iniziali scaricheranno solo una volta e saranno disponibili per tutti i portafogli Litecoin", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 301b0f6cc..0449a8749 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -36,6 +36,7 @@ "agree": "同意する", "agree_and_continue": "同意して続行", "agree_to": "アカウントを作成することにより、", + "alert_notice": "知らせ", "all": "すべて", "all_trades": "すべての取引", "all_transactions": "全取引", @@ -367,7 +368,12 @@ "litecoin_enable_mweb_sync": "MWEBスキャンを有効にします", "litecoin_mweb": "mweb", "litecoin_mweb_always_scan": "MWEBを常にスキャンします", + "litecoin_mweb_description": "MWEBは、Litecoinにより速く、より安価で、よりプライベートなトランザクションをもたらす新しいプロトコルです", + "litecoin_mweb_dismiss": "却下する", "litecoin_mweb_display_card": "MWEBカードを表示します", + "litecoin_mweb_enable_later": "表示設定の下で、MWEBを再度有効にすることを選択できます。", + "litecoin_mweb_pegin": "ペグイン", + "litecoin_mweb_pegout": "ペグアウト", "litecoin_mweb_scanning": "MWEBスキャン", "litecoin_mweb_settings": "MWEB設定", "litecoin_mweb_warning": "MWEBを使用すると、最初は〜600MBのデータをダウンロードし、ネットワーク速度に応じて最大30分かかる場合があります。この最初のデータは一度だけダウンロードされ、すべてのLitecoinウォレットで利用可能になります", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 5ae26757c..ccda2dbbc 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -36,6 +36,7 @@ "agree": "동의하다", "agree_and_continue": "동의 및 계속", "agree_to": "계정을 생성하면 ", + "alert_notice": "알아채다", "all": "모든", "all_trades": "A모든 거래", "all_transactions": "모든 거래 창구", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "mweb 스캔을 활성화합니다", "litecoin_mweb": "mweb", "litecoin_mweb_always_scan": "mweb는 항상 스캔을 설정합니다", + "litecoin_mweb_description": "MWEB는 Litecoin에 더 빠르고 저렴하며 개인 거래를 제공하는 새로운 프로토콜입니다.", + "litecoin_mweb_dismiss": "해고하다", "litecoin_mweb_display_card": "mweb 카드를 보여주십시오", + "litecoin_mweb_enable_later": "디스플레이 설정에서 MWEB를 다시 활성화하도록 선택할 수 있습니다.", + "litecoin_mweb_pegin": "페그를 입력하십시오", + "litecoin_mweb_pegout": "죽다", "litecoin_mweb_scanning": "mweb 스캔", "litecoin_mweb_settings": "mweb 설정", "litecoin_mweb_warning": "MWEB를 사용하면 처음에는 ~ 600MB의 데이터를 다운로드하며 네트워크 속도에 따라 최대 30 분이 소요될 수 있습니다. 이 초기 데이터는 한 번만 다운로드하여 모든 조명 지갑에 사용할 수 있습니다.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index b9c6441a7..b2c6386f0 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -36,6 +36,7 @@ "agree": "သဘောတူသည်။", "agree_and_continue": "သဘောတူပြီး ရှေ့ဆက်ပါ။", "agree_to": "အကောင့်ဖန်တီးခြင်းဖြင့် သင်သည် ဤအရာကို သဘောတူပါသည်။", + "alert_notice": "မှတ်သား", "all": "အားလုံး", "all_trades": "ကုန်သွယ်မှုအားလုံး", "all_transactions": "အရောင်းအဝယ်အားလုံး", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "mweb scanning ဖွင့်ပါ", "litecoin_mweb": "မင်္ဂလာပါ", "litecoin_mweb_always_scan": "Mweb အမြဲစကင်ဖတ်စစ်ဆေးပါ", + "litecoin_mweb_description": "Mweb သည် Protocol အသစ်ဖြစ်ပြီး LitCoin သို့ပိုမိုဈေးချိုသာသော, စျေးသက်သက်သာသာသုံးခြင်းနှင့်ပိုမိုများပြားသောပုဂ္ဂလိကငွေပို့ဆောင်မှုများကိုဖြစ်ပေါ်စေသည်", + "litecoin_mweb_dismiss": "ထုတ်ပစ်", "litecoin_mweb_display_card": "MweB ကဒ်ကိုပြပါ", + "litecoin_mweb_enable_later": "သင် MweB ကို display settings အောက်ရှိ ထပ်မံ. ခွင့်ပြုရန်ရွေးချယ်နိုင်သည်။", + "litecoin_mweb_pegin": "တံစို့", + "litecoin_mweb_pegout": "တံစို့", "litecoin_mweb_scanning": "mweb scanning", "litecoin_mweb_settings": "Mweb ဆက်တင်များ", "litecoin_mweb_warning": "MweB ကိုအသုံးပြုခြင်းသည်အစပိုင်းတွင် ~ 600MB ဒေတာများကို download လုပ်ပြီးကွန်ယက်အမြန်နှုန်းပေါ် မူတည်. မိနစ် 30 အထိကြာနိုင်သည်။ ဤကန ဦး ဒေတာကိုတစ်ကြိမ်သာ download လုပ်ပြီး litecoin Walkets အားလုံးအတွက်ရနိုင်သည်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index f447b9a3a..12315cd3c 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -36,6 +36,7 @@ "agree": "mee eens", "agree_and_continue": "Akkoord & doorgaan", "agree_to": "Door een account aan te maken gaat u akkoord met de ", + "alert_notice": "Kennisgeving", "all": "ALLE", "all_trades": "Alle transacties", "all_transactions": "Alle transacties", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "MWEB -scanning inschakelen", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Stel mweb altijd op scannen", + "litecoin_mweb_description": "MWEB is een nieuw protocol dat snellere, goedkopere en meer privé -transacties naar Litecoin brengt", + "litecoin_mweb_dismiss": "Afwijzen", "litecoin_mweb_display_card": "Toon MWEB -kaart", + "litecoin_mweb_enable_later": "U kunt ervoor kiezen om MWeb opnieuw in te schakelen onder weergave -instellingen.", + "litecoin_mweb_pegin": "Vastmaken", + "litecoin_mweb_pegout": "Uithakken", "litecoin_mweb_scanning": "MWEB -scanning", "litecoin_mweb_settings": "MWEB -instellingen", "litecoin_mweb_warning": "Het gebruik van MWeb downloadt in eerste instantie ~ 600 MB aan gegevens en kan tot 30 minuten duren, afhankelijk van de netwerksnelheid. Deze eerste gegevens worden slechts eenmaal gedownload en zijn beschikbaar voor alle Litecoin -portefeuilles", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index bcd420679..accb31d46 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -36,6 +36,7 @@ "agree": "Zgadzam się", "agree_and_continue": "Zgadzam się i kontynuuj", "agree_to": "Tworząc konto wyrażasz zgodę na ", + "alert_notice": "Ogłoszenie", "all": "WSZYSTKO", "all_trades": "Wszystkie operacje", "all_transactions": "Wszystkie transakcje", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Włącz skanowanie MWEB", "litecoin_mweb": "MWEB", "litecoin_mweb_always_scan": "Ustaw MWEB zawsze skanowanie", + "litecoin_mweb_description": "MWEB to nowy protokół, który przynosi szybciej, tańsze i bardziej prywatne transakcje do Litecoin", + "litecoin_mweb_dismiss": "Odrzucać", "litecoin_mweb_display_card": "Pokaż kartę MWEB", + "litecoin_mweb_enable_later": "Możesz ponownie włączyć MWEB w ustawieniach wyświetlania.", + "litecoin_mweb_pegin": "Kołek", + "litecoin_mweb_pegout": "Palikować", "litecoin_mweb_scanning": "Skanowanie MWEB", "litecoin_mweb_settings": "Ustawienia MWEB", "litecoin_mweb_warning": "Korzystanie z MWEB początkowo pobiera ~ 600 MB danych i może potrwać do 30 minut w zależności od prędkości sieci. Te początkowe dane pobierają tylko raz i będą dostępne dla wszystkich portfeli Litecoin", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 51d11f9b0..294a6bf1b 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -36,6 +36,7 @@ "agree": "Concordo", "agree_and_continue": "Concordar e continuar", "agree_to": "Ao criar conta você concorda com ", + "alert_notice": "Perceber", "all": "TUDO", "all_trades": "Todas as negociações", "all_transactions": "Todas as transacções", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Ativar digitalização do MWEB", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Definir mweb sempre digitalizando", + "litecoin_mweb_description": "MWEB é um novo protocolo que traz transações mais rápidas, baratas e mais privadas para o Litecoin", + "litecoin_mweb_dismiss": "Liberar", "litecoin_mweb_display_card": "Mostre o cartão MWEB", + "litecoin_mweb_enable_later": "Você pode optar por ativar o MWEB novamente em Configurações de exibição.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg fora", "litecoin_mweb_scanning": "MWEB Scanning", "litecoin_mweb_settings": "Configurações do MWEB", "litecoin_mweb_warning": "O uso do MWEB baixará inicialmente ~ 600 MB de dados e pode levar até 30 minutos, dependendo da velocidade da rede. Esses dados iniciais serão baixados apenas uma vez e estarão disponíveis para todas as carteiras Litecoin", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 94c8a2de3..1c3a23d67 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -36,6 +36,7 @@ "agree": "согласен", "agree_and_continue": "Согласиться и продолжить", "agree_to": "Создавая аккаунт, вы соглашаетесь с ", + "alert_notice": "Уведомление", "all": "ВСЕ", "all_trades": "Все сделки", "all_transactions": "Все транзакции", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Включить MWEB сканирование", "litecoin_mweb": "Мвеб", "litecoin_mweb_always_scan": "Установить MWEB всегда сканирование", + "litecoin_mweb_description": "MWEB - это новый протокол, который приносит быстрее, дешевле и более частные транзакции в Litecoin", + "litecoin_mweb_dismiss": "Увольнять", "litecoin_mweb_display_card": "Показать карту MWEB", + "litecoin_mweb_enable_later": "Вы можете снова включить MWEB в настройках отображения.", + "litecoin_mweb_pegin": "Внедрять", + "litecoin_mweb_pegout": "Выкрикивать", "litecoin_mweb_scanning": "MWEB сканирование", "litecoin_mweb_settings": "Настройки MWEB", "litecoin_mweb_warning": "Использование MWEB изначально загрузит ~ 600 МБ данных и может занять до 30 минут в зависимости от скорости сети. Эти начальные данные будут загружаться только один раз и будут доступны для всех кошельков Litecoin", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 8fb6b12cc..d1328257d 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -36,6 +36,7 @@ "agree": "ยอมรับ", "agree_and_continue": "ยอมรับและดำเนินการต่อ", "agree_to": "การสร้างบัญชีของคุณยอมรับเงื่อนไขของ", + "alert_notice": "สังเกต", "all": "ทั้งหมด", "all_trades": "การซื้อขายทั้งหมด", "all_transactions": "การทำธุรกรรมทั้งหมด", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "เปิดใช้งานการสแกน MWEB", "litecoin_mweb": "mweb", "litecoin_mweb_always_scan": "ตั้งค่าการสแกน MWEB เสมอ", + "litecoin_mweb_description": "MWEB เป็นโปรโตคอลใหม่ที่นำการทำธุรกรรมที่เร็วกว่าราคาถูกกว่าและเป็นส่วนตัวมากขึ้นไปยัง Litecoin", + "litecoin_mweb_dismiss": "อนุญาตให้ออกไป", "litecoin_mweb_display_card": "แสดงการ์ด mweb", + "litecoin_mweb_enable_later": "คุณสามารถเลือกเปิดใช้งาน MWEB อีกครั้งภายใต้การตั้งค่าการแสดงผล", + "litecoin_mweb_pegin": "หมุด", + "litecoin_mweb_pegout": "ตรึง", "litecoin_mweb_scanning": "การสแกน MWEB", "litecoin_mweb_settings": "การตั้งค่า MWEB", "litecoin_mweb_warning": "การใช้ MWEB จะดาวน์โหลดข้อมูล ~ 600MB ในขั้นต้นและอาจใช้เวลาสูงสุด 30 นาทีขึ้นอยู่กับความเร็วเครือข่าย ข้อมูลเริ่มต้นนี้จะดาวน์โหลดได้เพียงครั้งเดียวและพร้อมใช้งานสำหรับกระเป๋าเงินทั้งหมดของ Litecoin", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 1b699ba06..ca3a48478 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -36,6 +36,7 @@ "agree": "Sumang-ayon", "agree_and_continue": "Sumang-ayon & Magpatuloy", "agree_to": "Sa pamamagitan ng paggawa ng account sumasang-ayon ka sa ", + "alert_notice": "PAUNAWA", "all": "LAHAT", "all_trades": "Lahat ng mga trade", "all_transactions": "Lahat ng mga transaksyon", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Paganahin ang pag -scan ng MWeb", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Itakda ang MWeb na laging nag -scan", + "litecoin_mweb_description": "Ang MWeb ay isang bagong protocol na nagdadala ng mas mabilis, mas mura, at mas maraming pribadong mga transaksyon sa Litecoin", + "litecoin_mweb_dismiss": "Tanggalin", "litecoin_mweb_display_card": "Ipakita ang MWEB Card", + "litecoin_mweb_enable_later": "Maaari kang pumili upang paganahin muli ang MWeb sa ilalim ng mga setting ng pagpapakita.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg out", "litecoin_mweb_scanning": "Pag -scan ng Mweb", "litecoin_mweb_settings": "Mga Setting ng Mweb", "litecoin_mweb_warning": "Ang paggamit ng MWEB ay unang i -download ang ~ 600MB ng data, at maaaring tumagal ng hanggang sa 30 minuto depende sa bilis ng network. Ang paunang data na ito ay mag -download lamang ng isang beses at magagamit para sa lahat ng mga wallets ng Litecoin", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index c352192a8..dac8c1c81 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -36,6 +36,7 @@ "agree": "Kabul Et", "agree_and_continue": "Kabul Et & Devam Et", "agree_to": "Hesap oluşturarak bunları kabul etmiş olursunuz ", + "alert_notice": "Fark etme", "all": "HEPSİ", "all_trades": "Tüm takaslar", "all_transactions": "Tüm transferler", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "MWEB taramasını etkinleştir", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "MWEB'i her zaman taramayı ayarlayın", + "litecoin_mweb_description": "MWEB, Litecoin'e daha hızlı, daha ucuz ve daha fazla özel işlem getiren yeni bir protokoldür", + "litecoin_mweb_dismiss": "Azletmek", "litecoin_mweb_display_card": "MWEB kartını göster", + "litecoin_mweb_enable_later": "Ekran ayarlarının altında MWEB'yi tekrar etkinleştirmeyi seçebilirsiniz.", + "litecoin_mweb_pegin": "Takılmak", + "litecoin_mweb_pegout": "Çiğnemek", "litecoin_mweb_scanning": "MWEB taraması", "litecoin_mweb_settings": "MWEB ayarları", "litecoin_mweb_warning": "MWEB kullanmak başlangıçta ~ 600MB veri indirir ve ağ hızına bağlı olarak 30 dakikaya kadar sürebilir. Bu ilk veriler yalnızca bir kez indirilecek ve tüm Litecoin cüzdanları için kullanılabilir olacak", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index bb8dce457..ba6c6516d 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -36,6 +36,7 @@ "agree": "Згоден", "agree_and_continue": "Погодитися та продовжити", "agree_to": "Створюючи обліковий запис, ви погоджуєтеся з ", + "alert_notice": "Ув'язнення", "all": "ВСЕ", "all_trades": "Всі операції", "all_transactions": "Всі транзакції", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Увімкнути сканування MWEB", "litecoin_mweb": "Мвеб", "litecoin_mweb_always_scan": "Встановити mweb завжди сканувати", + "litecoin_mweb_description": "MWEB - це новий протокол, який приносить швидкі, дешевші та більш приватні транзакції Litecoin", + "litecoin_mweb_dismiss": "Звільнити", "litecoin_mweb_display_card": "Показати карту MWeb", + "litecoin_mweb_enable_later": "Ви можете знову ввімкнути MWEB в налаштуваннях дисплея.", + "litecoin_mweb_pegin": "Подякувати", + "litecoin_mweb_pegout": "Подякувати", "litecoin_mweb_scanning": "Сканування Mweb", "litecoin_mweb_settings": "Налаштування MWEB", "litecoin_mweb_warning": "Використання MWEB спочатку завантажить ~ 600 Мб даних і може зайняти до 30 хвилин залежно від швидкості мережі. Ці початкові дані завантажуються лише один раз і будуть доступні для всіх гаманців Litecoin", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index da3d04a04..3e68a5fb4 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -36,6 +36,7 @@ "agree": "متفق", "agree_and_continue": "اتفاق کریں اور جاری رکھیں", "agree_to": "اکاؤنٹ بنا کر آپ اس سے اتفاق کرتے ہیں۔", + "alert_notice": "نوٹس", "all": "تمام", "all_trades": "تمام تجارت", "all_transactions": "تمام لین دین", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "MWEB اسکیننگ کو فعال کریں", "litecoin_mweb": "MWEB", "litecoin_mweb_always_scan": "MWEB ہمیشہ اسکیننگ سیٹ کریں", + "litecoin_mweb_description": "MWEB ایک نیا پروٹوکول ہے جو لیٹیکوئن میں تیز ، سستا اور زیادہ نجی لین دین لاتا ہے", + "litecoin_mweb_dismiss": "خارج", "litecoin_mweb_display_card": "MWEB کارڈ دکھائیں", + "litecoin_mweb_enable_later": "آپ ڈسپلے کی ترتیبات کے تحت MWEB کو دوبارہ فعال کرنے کا انتخاب کرسکتے ہیں۔", + "litecoin_mweb_pegin": "پیگ میں", + "litecoin_mweb_pegout": "پیگ آؤٹ", "litecoin_mweb_scanning": "MWEB اسکیننگ", "litecoin_mweb_settings": "MWEB کی ترتیبات", "litecoin_mweb_warning": "MWEB کا استعمال ابتدائی طور پر m 600mb ڈیٹا ڈاؤن لوڈ کرے گا ، اور نیٹ ورک کی رفتار کے لحاظ سے 30 منٹ تک کا وقت لگ سکتا ہے۔ یہ ابتدائی اعداد و شمار صرف ایک بار ڈاؤن لوڈ کریں گے اور تمام لیٹیکوئن بٹوے کے لئے دستیاب ہوں گے", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index f4ed7aebb..159a127d0 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -36,6 +36,7 @@ "agree": "Đồng ý", "agree_and_continue": "Đồng ý & Tiếp tục", "agree_to": "Bằng cách tạo tài khoản, bạn đồng ý với ", + "alert_notice": "Để ý", "all": "TẤT CẢ", "all_trades": "Tất cả giao dịch", "all_transactions": "Tất cả giao dịch", @@ -364,6 +365,11 @@ "ledger_error_wrong_app": "Vui lòng đảm bảo bạn đã mở đúng ứng dụng trên Ledger của mình", "ledger_please_enable_bluetooth": "Vui lòng bật Bluetooth để phát hiện Ledger của bạn", "light_theme": "Chủ đề sáng", + "litecoin_mweb_description": "MWEB là một giao thức mới mang lại các giao dịch nhanh hơn, rẻ hơn và riêng tư hơn cho Litecoin", + "litecoin_mweb_dismiss": "Miễn nhiệm", + "litecoin_mweb_enable_later": "Bạn có thể chọn bật lại MWEB trong cài đặt hiển thị.", + "litecoin_mweb_pegin": "Chốt vào", + "litecoin_mweb_pegout": "Chốt ra", "live_fee_rates": "Tỷ lệ phí hiện tại qua API", "load_more": "Tải thêm", "loading_your_wallet": "Đang tải ví của bạn", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 8114d5ab1..a12774d5d 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -36,6 +36,7 @@ "agree": "Jọ rò", "agree_and_continue": "Jọ Rò àti Tẹ̀síwájú", "agree_to": "Tẹ́ ẹ bá dá àkáǹtì ẹ jọ rò ", + "alert_notice": "Akiyesi", "all": "Gbogbo", "all_trades": "Gbogbo àwọn pàṣípààrọ̀", "all_transactions": "Gbogbo àwọn àránṣẹ́", @@ -367,7 +368,12 @@ "litecoin_enable_mweb_sync": "Mu mweb ọlọjẹ", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Ṣeto mweb nigbagbogbo n ṣayẹwo", + "litecoin_mweb_description": "Mweb jẹ ilana ilana tuntun ti o mu iyara wa yiyara, din owo, ati awọn iṣowo ikọkọ diẹ sii si Livcoin", + "litecoin_mweb_dismiss": "Tuka", "litecoin_mweb_display_card": "Fihan kaadi Mweb", + "litecoin_mweb_enable_later": "O le yan lati ṣiṣẹ Mweb lẹẹkansi labẹ awọn eto ifihan.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg jade", "litecoin_mweb_scanning": "Mweb scanning", "litecoin_mweb_settings": "Awọn eto Mweb", "litecoin_mweb_warning": "Lilo Mweb yoo wa lakoko igbasilẹ ~ 600MB ti data, o le gba to iṣẹju 30 da lori iyara nẹtiwọọki. Awọn data akọkọ yii yoo ṣe igbasilẹ lẹẹkan si ki o wa fun gbogbo awọn Wolinkun LiveCooin", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index de1bb4df4..8f8e8cd13 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -36,6 +36,7 @@ "agree": "同意", "agree_and_continue": "同意并继续", "agree_to": "创建账户即表示您同意 ", + "alert_notice": "注意", "all": "全部", "all_trades": "所有的变化", "all_transactions": "所有交易", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "启用MWEB扫描", "litecoin_mweb": "MWEB", "litecoin_mweb_always_scan": "设置MWEB总是扫描", + "litecoin_mweb_description": "MWEB是一项新协议,它将更快,更便宜和更多的私人交易带给Litecoin", + "litecoin_mweb_dismiss": "解雇", "litecoin_mweb_display_card": "显示MWEB卡", + "litecoin_mweb_enable_later": "您可以选择在显示设置下再次启用MWEB。", + "litecoin_mweb_pegin": "钉进", + "litecoin_mweb_pegout": "昏倒", "litecoin_mweb_scanning": "MWEB扫描", "litecoin_mweb_settings": "MWEB设置", "litecoin_mweb_warning": "使用MWEB最初将下载约600MB的数据,并且最多可能需要30分钟的时间,具体取决于网络速度。此初始数据只能下载一次,并适用于所有莱特币钱包", diff --git a/tool/configure.dart b/tool/configure.dart index 0c0f491a5..1e2741721 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -107,6 +107,7 @@ import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -230,6 +231,7 @@ abstract class Bitcoin { bool txIsMweb(TransactionInfo txInfo); Future setMwebEnabled(Object wallet, bool enabled); bool getMwebEnabled(Object wallet); + String? getUnusedMwebAddress(Object wallet); } """; From 62f55ae8f51d6697f658c38e541dd055b392ec41 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 8 Oct 2024 01:07:27 +0100 Subject: [PATCH 117/142] Allow Wallet Group Names to be the same as Wallet Names (#1730) * fix: Issues with imaging * fix: Allow group names to be the same as wallet names * fix: Bug with wallet grouping when a wallet is minimized * fix: Bug with wallet grouping when a wallet is minimized --- lib/di.dart | 6 +++++- lib/src/screens/wallet/wallet_edit_page.dart | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index 5e5417b34..0008ad8fd 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -378,7 +378,11 @@ Future setup({ getIt.registerFactory(() => NewWalletTypeViewModel(_walletInfoSource)); getIt.registerFactory( - () => WalletManager(_walletInfoSource, getIt.get()), + () { + final instance = WalletManager(_walletInfoSource, getIt.get()); + instance.updateWalletGroups(); + return instance; + }, ); getIt.registerFactoryParam( diff --git a/lib/src/screens/wallet/wallet_edit_page.dart b/lib/src/screens/wallet/wallet_edit_page.dart index 9515e58c3..340091a1e 100644 --- a/lib/src/screens/wallet/wallet_edit_page.dart +++ b/lib/src/screens/wallet/wallet_edit_page.dart @@ -18,7 +18,6 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; - class WalletEditPage extends BasePage { WalletEditPage({ required this.pageArguments, @@ -86,8 +85,9 @@ class WalletEditPage extends BasePage { child: LoadingPrimaryButton( onPressed: () async { if (_formKey.currentState?.validate() ?? false) { - if (pageArguments.walletNewVM! - .nameExists(walletEditViewModel.newName)) { + if (!pageArguments.isWalletGroup && + pageArguments.walletNewVM! + .nameExists(walletEditViewModel.newName)) { showPopUp( context: context, builder: (_) { From 8acf8bdfb21cabad9cd16c296465d01199b54a51 Mon Sep 17 00:00:00 2001 From: Rafael Date: Thu, 10 Oct 2024 11:10:12 -0300 Subject: [PATCH 118/142] feat: add onError exception (#1743) --- cw_bitcoin/lib/electrum_wallet.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 877b108b7..0be83c624 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1315,7 +1315,7 @@ abstract class ElectrumWalletBase // Set the balance of all non-silent payment addresses to 0 before updating walletAddresses.allAddresses.forEach((addr) { - if(addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; }); await Future.wait(walletAddresses.allAddresses.map((address) async { @@ -1825,6 +1825,8 @@ abstract class ElectrumWalletBase Future> _fetchAddressHistory( BitcoinAddressRecord addressRecord, int? currentHeight) async { + String txid = ""; + try { final Map historiesWithDetails = {}; @@ -1834,7 +1836,7 @@ abstract class ElectrumWalletBase addressRecord.setAsUsed(); await Future.wait(history.map((transaction) async { - final txid = transaction['tx_hash'] as String; + txid = transaction['tx_hash'] as String; final height = transaction['height'] as int; final storedTx = transactionHistory.transactions[txid]; @@ -1865,8 +1867,12 @@ abstract class ElectrumWalletBase } return historiesWithDetails; - } catch (e) { - print(e.toString()); + } catch (e, stacktrace) { + _onError?.call(FlutterErrorDetails( + exception: "$txid - $e", + stack: stacktrace, + library: this.runtimeType.toString(), + )); return {}; } } From 380f7653b2e39f468d722424b8e4d7f0f0eb910f Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sun, 13 Oct 2024 02:13:52 +0300 Subject: [PATCH 119/142] V4.20.0 rc2 (#1727) * version 4.20.0 * update build numbers * UI updates and script fix for ios bundle identifier * disable mweb for desktop * change hardcoded ltc server ip address electrum connection enhancement * MWEB enhancements 2.0 (#1735) * additional logging and minor fixes * additional logging and minor fixes * addresses pt.1 * Allow Wallet Group Names to be the same as Wallet Names (#1730) * fix: Issues with imaging * fix: Allow group names to be the same as wallet names * fix: Bug with wallet grouping when a wallet is minimized * fix: Bug with wallet grouping when a wallet is minimized * logs of fixes and experimental changes, close wallet before opening next * save * fix icon * fixes * [skip ci] updates * [skip ci] updates * updates * minor optimizations * fix for when switching between wallets * [skip ci] updates * [skip ci] updates * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * mobx * mostly logging * stream fix pt.1 [skip ci] * updates * some fixes and enhancements * [skip ci] minor * potential partial fix for streamsink closed * fix stream sink closed errors * fix mweb logo colors * save * minor enhancements [skip ci] * save * experimental * minor * minor [skip ci] --------- Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: Omar Hatem * fix menu list removing from original list --------- Co-authored-by: Matthew Fosse Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> --- assets/text/Monerocom_Release_Notes.txt | 6 +- assets/text/Release_Notes.txt | 13 +- .../lib/bitcoin_receive_page_option.dart | 2 +- cw_bitcoin/lib/electrum.dart | 13 +- cw_bitcoin/lib/electrum_wallet.dart | 30 ++-- cw_bitcoin/lib/electrum_wallet_addresses.dart | 6 +- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 11 +- cw_bitcoin/lib/litecoin_wallet.dart | 60 ++++---- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 55 +++---- .../lib/pending_bitcoin_transaction.dart | 2 + cw_core/lib/wallet_base.dart | 2 +- cw_evm/lib/evm_chain_wallet.dart | 2 +- cw_haven/lib/haven_wallet.dart | 2 +- cw_haven/lib/haven_wallet_service.dart | 2 +- cw_monero/lib/monero_wallet.dart | 2 +- cw_monero/lib/monero_wallet_service.dart | 2 +- .../com/cakewallet/mweb/CwMwebPlugin.kt | 11 +- cw_mweb/ios/Classes/CwMwebPlugin.swift | 17 ++- cw_mweb/lib/cw_mweb.dart | 135 +++++++++++++----- cw_mweb/lib/cw_mweb_method_channel.dart | 25 ++++ cw_mweb/lib/cw_mweb_platform_interface.dart | 4 + cw_nano/lib/nano_wallet.dart | 2 +- cw_nano/pubspec.lock | 4 +- cw_solana/lib/solana_wallet.dart | 2 +- cw_tron/lib/tron_wallet.dart | 2 +- cw_wownero/lib/wownero_wallet.dart | 2 +- cw_wownero/lib/wownero_wallet_service.dart | 2 +- ios/Podfile.lock | 46 +++++- ios/Runner.xcodeproj/project.pbxproj | 54 +++---- lib/bitcoin/cw_bitcoin.dart | 15 +- .../screens/dashboard/pages/balance_page.dart | 121 ++++++++-------- .../dashboard/widgets/menu_widget.dart | 14 +- lib/src/screens/root/root.dart | 7 - lib/src/screens/settings/mweb_settings.dart | 5 +- lib/src/widgets/setting_actions.dart | 3 +- lib/store/app_store.dart | 3 +- .../dashboard/balance_view_model.dart | 37 ++++- .../dashboard/dashboard_view_model.dart | 39 ++--- .../settings/mweb_settings_view_model.dart | 14 +- lib/view_model/wallet_creation_vm.dart | 48 ++++--- .../wallet_list/wallet_list_view_model.dart | 2 + macos/Podfile.lock | 8 +- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 3 +- res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + scripts/android/app_env.sh | 8 +- scripts/android/build_mwebd.sh | 2 +- scripts/ios/app_config.sh | 2 +- scripts/ios/app_env.sh | 8 +- scripts/ios/build_mwebd.sh | 2 +- scripts/linux/app_env.sh | 4 +- scripts/macos/app_config.sh | 2 +- scripts/macos/app_env.sh | 8 +- scripts/windows/build_exe_installer.iss | 2 +- tool/configure.dart | 1 + 80 files changed, 572 insertions(+), 329 deletions(-) diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index fec1485ac..2f79dd5c6 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,2 +1,4 @@ -Enhance auto-address generation for Monero -Bug fixes and enhancements \ No newline at end of file +Monero enhancements for sending and address generation +StealthEx +LetsExchange +Visual enhancements and bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 4e8a79fc1..868ae7954 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,4 +1,9 @@ -Enable BIP39 by default for wallet creation also on Bitcoin/Litecoin (Electrum seed type is still accessible through advanced settings page) -Improve fee calculation for Bitcoin to protect against overpaying or underpaying -Enhance auto-address generation for Monero -Bug fixes and enhancements \ No newline at end of file +Add Litecoin MWEB +Wallet groups (same seed, multiple wallets) +Silent Payments enhancements +Monero enhancements for sending and address generation +StealthEx +LetsExchange +Replace-By-Fee improvements +ERC20 tokens potential scam detection +Visual enhancements and bug fixes \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 8e72bc7b0..07083e111 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -30,7 +30,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const allLitecoin = [ BitcoinReceivePageOption.p2wpkh, - BitcoinReceivePageOption.mweb + BitcoinReceivePageOption.mweb, ]; BitcoinAddressType toType() { diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index cfcc71d09..54ccaaef5 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -116,7 +116,7 @@ class ElectrumClient { _parseResponse(message); } } catch (e) { - print(e.toString()); + print("socket.listen: $e"); } }, onError: (Object error) { @@ -125,14 +125,15 @@ class ElectrumClient { unterminatedString = ''; }, onDone: () { + print("SOCKET CLOSED!!!!!"); unterminatedString = ''; try { if (host == socket?.address.host) { - socket?.destroy(); _setConnectionStatus(ConnectionStatus.disconnected); + socket?.destroy(); } } catch (e) { - print(e.toString()); + print("onDone: $e"); } }, cancelOnError: true, @@ -421,7 +422,7 @@ class ElectrumClient { BehaviorSubject? subscribe( {required String id, required String method, List params = const []}) { try { - if (socket == null) { + if (socket == null || !isConnected) { return null; } final subscription = BehaviorSubject(); @@ -437,7 +438,7 @@ class ElectrumClient { Future call( {required String method, List params = const [], Function(int)? idCallback}) async { - if (socket == null) { + if (socket == null || !isConnected) { return null; } final completer = Completer(); @@ -453,7 +454,7 @@ class ElectrumClient { Future callWithTimeout( {required String method, List params = const [], int timeout = 5000}) async { try { - if (socket == null) { + if (socket == null || !isConnected) { return null; } final completer = Completer(); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 0be83c624..97897046c 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -23,7 +22,6 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; -import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -168,7 +166,10 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - Set get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + Set get addressesSet => walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((addr) => addr.address) + .toSet(); List get scriptHashes => walletAddresses.addressesByReceiveType .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) @@ -1174,6 +1175,7 @@ abstract class ElectrumWalletBase 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(), + 'alwaysScan': alwaysScan, }); int feeRate(TransactionPriority priority) { @@ -1291,7 +1293,7 @@ abstract class ElectrumWalletBase } @override - Future close() async { + Future close({required bool shouldCleanup}) async { try { await _receiveStream?.cancel(); await electrumClient.close(); @@ -1314,11 +1316,15 @@ abstract class ElectrumWalletBase } // Set the balance of all non-silent payment addresses to 0 before updating - walletAddresses.allAddresses.forEach((addr) { + walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .forEach((addr) { if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; }); - await Future.wait(walletAddresses.allAddresses.map((address) async { + await Future.wait(walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((address) async { updatedUnspentCoins.addAll(await fetchUnspent(address)); })); @@ -1878,6 +1884,7 @@ abstract class ElectrumWalletBase } Future updateTransactions() async { + print("updateTransactions() called!"); try { if (_isTransactionUpdating) { return; @@ -1903,12 +1910,16 @@ abstract class ElectrumWalletBase Future subscribeForUpdates() async { final unsubscribedScriptHashes = walletAddresses.allAddresses.where( - (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)), + (address) => + !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)) && + address.type != SegwitAddresType.mweb, ); await Future.wait(unsubscribedScriptHashes.map((address) async { final sh = address.getScriptHash(network); - await _scripthashesUpdateSubject[sh]?.close(); + if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { + await _scripthashesUpdateSubject[sh]?.close(); + } _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { @@ -1918,7 +1929,7 @@ abstract class ElectrumWalletBase await _fetchAddressHistory(address, await getCurrentChainTip()); } catch (e, s) { - print(e.toString()); + print("sub error: $e"); _onError?.call(FlutterErrorDetails( exception: e, stack: s, @@ -1996,6 +2007,7 @@ abstract class ElectrumWalletBase } Future updateBalance() async { + print("updateBalance() called!"); balance[currency] = await fetchBalances(); await save(); } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 91345d726..ce1ff9713 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,3 +1,5 @@ +import 'dart:io' show Platform; + import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -238,7 +240,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); - await _generateInitialAddresses(type: SegwitAddresType.mweb); + if (Platform.isAndroid || Platform.isIOS) { + await _generateInitialAddresses(type: SegwitAddresType.mweb); + } } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 25cc5637e..990719089 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -24,6 +24,7 @@ class ElectrumWalletSnapshot { required this.silentAddresses, required this.silentAddressIndex, required this.mwebAddresses, + required this.alwaysScan, this.passphrase, this.derivationType, this.derivationPath, @@ -46,6 +47,7 @@ class ElectrumWalletSnapshot { List addresses; List silentAddresses; List mwebAddresses; + bool alwaysScan; ElectrumBalance balance; Map regularAddressIndex; @@ -54,15 +56,15 @@ class ElectrumWalletSnapshot { DerivationType? derivationType; String? derivationPath; - static Future load( - EncryptionFileUtils encryptionFileUtils, String name, WalletType type, String password, BasedUtxoNetwork network) async { + static Future load(EncryptionFileUtils encryptionFileUtils, String name, + WalletType type, String password, BasedUtxoNetwork network) async { final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; final mnemonic = data['mnemonic'] as String?; final xpub = data['xpub'] as String?; final passphrase = data['passphrase'] as String? ?? ''; - + final addressesTmp = data['addresses'] as List? ?? []; final addresses = addressesTmp .whereType() @@ -81,6 +83,8 @@ class ElectrumWalletSnapshot { .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) .toList(); + final alwaysScan = data['alwaysScan'] as bool? ?? false; + final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; @@ -124,6 +128,7 @@ class ElectrumWalletSnapshot { silentAddresses: silentAddresses, silentAddressIndex: silentAddressIndex, mwebAddresses: mwebAddresses, + alwaysScan: alwaysScan, ); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 8fe02ecd3..f4d99d807 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -76,6 +76,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, currency: CryptoCurrency.ltc, + alwaysScan: alwaysScan, ) { mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; mwebEnabled = alwaysScan ?? false; @@ -101,7 +102,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Timer? _feeRatesTimer; Timer? _processingTimer; StreamSubscription? _utxoStream; - late RpcClient _stub; late bool mwebEnabled; bool processingUtxos = false; @@ -229,18 +229,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, - alwaysScan: alwaysScan, + alwaysScan: snp?.alwaysScan, ); } Future waitForMwebAddresses() async { + print("waitForMwebAddresses() called!"); // ensure that we have the full 1000 mweb addresses generated before continuing: // should no longer be needed, but leaving here just in case - // final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; - // while (mwebAddrs.length < 1000) { - // print("waiting for mweb addresses to finish generating..."); - // await Future.delayed(const Duration(milliseconds: 1000)); - // } await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020); } @@ -252,16 +248,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + _syncTimer?.cancel(); try { syncStatus = SyncronizingSyncStatus(); await subscribeForUpdates(); updateFeeRates(); - _feeRatesTimer?.cancel(); _feeRatesTimer = Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); if (!mwebEnabled) { + try { + // in case we're switching from a litecoin wallet that had mweb enabled + CwMweb.stop(); + } catch (_) {} try { await updateAllUnspents(); await updateTransactions(); @@ -276,23 +276,19 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } await waitForMwebAddresses(); - await getStub(); await processMwebUtxos(); await updateTransactions(); await updateUnspent(); await updateBalance(); } catch (e) { print("failed to start mweb sync: $e"); - syncStatus = FailedSyncStatus(); + syncStatus = FailedSyncStatus(error: "failed to start"); return; } - _syncTimer?.cancel(); - _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { + _syncTimer = Timer.periodic(const Duration(milliseconds: 3000), (timer) async { if (syncStatus is FailedSyncStatus) return; - print("SYNCING...."); - final nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node @@ -350,10 +346,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @action @override Future stopSync() async { + print("stopSync() called!"); _syncTimer?.cancel(); _utxoStream?.cancel(); _feeRatesTimer?.cancel(); await CwMweb.stop(); + print("stopped syncing!"); } Future initMwebUtxosBox() async { @@ -425,9 +423,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await initMwebUtxosBox(); } - Future handleIncoming(MwebUtxo utxo, RpcClient stub) async { + Future handleIncoming(MwebUtxo utxo) async { print("handleIncoming() called!"); - final status = await stub.status(StatusRequest()); + final status = await CwMweb.status(StatusRequest()); var date = DateTime.now(); var confirmations = 0; if (utxo.height > 0) { @@ -493,6 +491,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future processMwebUtxos() async { + print("processMwebUtxos() called!"); if (!mwebEnabled) { return; } @@ -502,7 +501,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight); // process new utxos as they come in: - _utxoStream?.cancel(); + await _utxoStream?.cancel(); ResponseStream? responseStream = await CwMweb.utxos(req); if (responseStream == null) { throw Exception("failed to get utxos stream!"); @@ -544,7 +543,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await mwebUtxosBox.put(utxo.outputId, utxo); - await handleIncoming(utxo, _stub); + await handleIncoming(utxo); }); } @@ -662,6 +661,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future updateUnspent() async { + print("updateUnspent() called!"); await checkMwebUtxosSpent(); await updateAllUnspents(); } @@ -675,7 +675,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { return; } - await getStub(); // add the mweb unspents to the list: List mwebUnspentCoins = []; @@ -718,7 +717,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { return balance; } - await getStub(); // update unspent balances: await updateUnspent(); @@ -891,7 +889,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return tx; } await waitForMwebAddresses(); - await getStub(); final resp = await CwMweb.create(CreateRequest( rawTx: hex.decode(tx.hex), @@ -1000,13 +997,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } @override - Future close() async { + Future close({required bool shouldCleanup}) async { _utxoStream?.cancel(); _feeRatesTimer?.cancel(); _syncTimer?.cancel(); _processingTimer?.cancel(); - await stopSync(); - await super.close(); + if (shouldCleanup) { + try { + await stopSync(); + } catch (_) {} + } + await super.close(shouldCleanup: shouldCleanup); } Future setMwebEnabled(bool enabled) async { @@ -1014,17 +1015,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } + alwaysScan = enabled; mwebEnabled = enabled; (walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled; - await stopSync(); + await save(); + try { + await stopSync(); + } catch (_) {} await startSync(); } - Future getStub() async { - _stub = await CwMweb.stub(); - return _stub; - } - Future getStatusRequest() async { final resp = await CwMweb.status(StatusRequest()); return resp; diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 88f640d85..0a20665bf 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; @@ -39,8 +40,10 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with bool mwebEnabled; int mwebTopUpIndex = 1000; List mwebAddrs = []; + bool generating = false; List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendPubkey => mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @@ -57,20 +60,38 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with } Future ensureMwebAddressUpToIndexExists(int index) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } + Uint8List scan = Uint8List.fromList(scanSecret); Uint8List spend = Uint8List.fromList(spendPubkey); - int count = 0; - while (mwebAddrs.length <= (index + 1)) { - final address = await CwMweb.address(scan, spend, mwebAddrs.length); - mwebAddrs.add(address!); - count++; - // sleep for a bit to avoid making the main thread unresponsive: - if (count > 50) { - count = 0; - await Future.delayed(Duration(milliseconds: 100)); - } + + if (index < mwebAddresses.length && index < mwebAddrs.length) { + return; } + while (generating) { + print("generating....."); + // this function was called multiple times in multiple places: + await Future.delayed(const Duration(milliseconds: 100)); + } + + print("Generating MWEB addresses up to index $index"); + generating = true; + try { + while (mwebAddrs.length <= (index + 1)) { + final addresses = + await CwMweb.addresses(scan, spend, mwebAddrs.length, mwebAddrs.length + 50); + print("generated up to index ${mwebAddrs.length}"); + // sleep for a bit to avoid making the main thread unresponsive: + await Future.delayed(Duration(milliseconds: 200)); + mwebAddrs.addAll(addresses!); + } + } catch (_) {} + generating = false; + print("Done generating MWEB addresses len: ${mwebAddrs.length}"); + // ensure mweb addresses are up to date: if (mwebAddresses.length < mwebAddrs.length) { List addressRecords = mwebAddrs @@ -90,21 +111,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Future initMwebAddresses() async { if (mwebAddrs.length < 1000) { - print("Generating MWEB addresses..."); await ensureMwebAddressUpToIndexExists(20); - print("done generating MWEB addresses"); - // List addressRecords = mwebAddrs - // .asMap() - // .entries - // .map((e) => BitcoinAddressRecord( - // e.value, - // index: e.key, - // type: SegwitAddresType.mweb, - // network: network, - // )) - // .toList(); - // addMwebAddresses(addressRecords); - // print("added ${addressRecords.length} mweb addresses"); return; } } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 30cc29ec9..c722dc14f 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -117,6 +117,8 @@ class PendingBitcoinTransaction with PendingTransaction { idOverride = resp.txid; } on GrpcError catch (e) { throw BitcoinTransactionCommitFailed(errorMessage: e.message); + } catch (e) { + throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}"); } } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 4df1aa1f9..112a20852 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -83,7 +83,7 @@ abstract class WalletBase rescan({required int height}); - void close(); + Future close({required bool shouldCleanup}); Future changePassword(String password); diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 9bc18d14b..cfaf39d98 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -264,7 +264,7 @@ abstract class EVMChainWalletBase } @override - void close() { + Future close({required bool shouldCleanup}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); _updateFeesTimer?.cancel(); diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 317d9dc65..06a838100 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -106,7 +106,7 @@ abstract class HavenWalletBase Future? updateBalance() => null; @override - void close() { + Future close({required bool shouldCleanup}) async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); diff --git a/cw_haven/lib/haven_wallet_service.dart b/cw_haven/lib/haven_wallet_service.dart index 14bc520da..274ddc4f8 100644 --- a/cw_haven/lib/haven_wallet_service.dart +++ b/cw_haven/lib/haven_wallet_service.dart @@ -116,7 +116,7 @@ class HavenWalletService extends WalletService< if (!isValid) { await restoreOrResetWalletFiles(name); - wallet.close(); + wallet.close(shouldCleanup: false); return openWallet(name, password); } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index c6d5d2e5f..0ae2202ba 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -170,7 +170,7 @@ abstract class MoneroWalletBase extends WalletBase? updateBalance() => null; @override - void close() async { + Future close({required bool shouldCleanup}) async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); _onTxHistoryChangeReaction?.reaction.dispose(); diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 7f4672d43..63ad17583 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -137,7 +137,7 @@ class MoneroWalletService extends WalletService< if (!isValid) { await restoreOrResetWalletFiles(name); - wallet.close(); + wallet.close(shouldCleanup: false); return openWallet(name, password); } diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt index 0cc4cc537..3ab24ee12 100644 --- a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -40,10 +40,17 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { port = null result.success(null) } else if (call.method == "address") { + // val scanSecret: ByteArray = call.argument("scanSecret") ?: ByteArray(0) + // val spendPub: ByteArray = call.argument("spendPub") ?: ByteArray(0) + // val index: Int = call.argument("index") ?: 0 + // val res = Mwebd.address(scanSecret, spendPub, index) + // result.success(res) + } else if (call.method == "addresses") { val scanSecret: ByteArray = call.argument("scanSecret") ?: ByteArray(0) val spendPub: ByteArray = call.argument("spendPub") ?: ByteArray(0) - val index: Int = call.argument("index") ?: 0 - val res = Mwebd.address(scanSecret, spendPub, index) + val fromIndex: Int = call.argument("fromIndex") ?: 0 + val toIndex: Int = call.argument("toIndex") ?: 0 + val res = Mwebd.addresses(scanSecret, spendPub, fromIndex, toIndex) result.success(res) } else { result.notImplemented() diff --git a/cw_mweb/ios/Classes/CwMwebPlugin.swift b/cw_mweb/ios/Classes/CwMwebPlugin.swift index 92061ded8..55b380829 100644 --- a/cw_mweb/ios/Classes/CwMwebPlugin.swift +++ b/cw_mweb/ios/Classes/CwMwebPlugin.swift @@ -32,15 +32,26 @@ public static func register(with registrar: FlutterPluginRegistrar) { stopServer() result(nil) break - case "address": + // case "address": + // let args = call.arguments as! [String: Any] + // let scanSecret = args["scanSecret"] as! FlutterStandardTypedData + // let spendPub = args["spendPub"] as! FlutterStandardTypedData + // let index = args["index"] as! Int32 + + // let scanSecretData = scanSecret.data + // let spendPubData = spendPub.data + // result(MwebdAddress(scanSecretData, spendPubData, index)) + // break + case "addresses": let args = call.arguments as! [String: Any] let scanSecret = args["scanSecret"] as! FlutterStandardTypedData let spendPub = args["spendPub"] as! FlutterStandardTypedData - let index = args["index"] as! Int32 + let fromIndex = args["fromIndex"] as! Int32 + let toIndex = args["toIndex"] as! Int32 let scanSecretData = scanSecret.data let spendPubData = spendPub.data - result(MwebdAddress(scanSecretData, spendPubData, index)) + result(MwebdAddresses(scanSecretData, spendPubData, fromIndex, toIndex)) break default: result(FlutterMethodNotImplemented) diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index 3be8e9bf5..39aa433cd 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -1,3 +1,7 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; import 'dart:typed_data'; import 'package:grpc/grpc.dart'; @@ -10,25 +14,51 @@ class CwMweb { static ClientChannel? _clientChannel; static int? _port; static const TIMEOUT_DURATION = Duration(seconds: 5); + static Timer? logTimer; + + static void readFileWithTimer(String filePath) { + final file = File(filePath); + int lastLength = 0; + + logTimer?.cancel(); + logTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + try { + final currentLength = await file.length(); + + if (currentLength != lastLength) { + final fileStream = file.openRead(lastLength, currentLength); + final newLines = await fileStream.transform(utf8.decoder).join(); + lastLength = currentLength; + log(newLines); + } + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log('The mwebd debug log probably is not initialized yet.'); + } + }); + } static Future _initializeClient() async { - await stop(); - // wait a few seconds to make sure the server is stopped - await Future.delayed(const Duration(seconds: 5)); - + print("initialize client called!"); final appDir = await getApplicationSupportDirectory(); - const ltcNodeUri = "45.79.13.180:9333"; + const ltcNodeUri = "ltc-electrum.cakewallet.com:9333"; + + String debugLogPath = "${appDir.path}/logs/debug.log"; + readFileWithTimer(debugLogPath); + _port = await CwMwebPlatform.instance.start(appDir.path, ltcNodeUri); if (_port == null || _port == 0) { throw Exception("Failed to start server"); } - print("Attempting to connect to server on port: $_port"); + log("Attempting to connect to server on port: $_port"); // wait for the server to finish starting up before we try to connect to it: await Future.delayed(const Duration(seconds: 5)); _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () { - print("Channel is shutting down!"); + _rpcClient = null; + log("Channel is shutting down!"); }, options: const ChannelOptions( credentials: ChannelCredentials.insecure(), @@ -49,9 +79,15 @@ class CwMweb { throw Exception("blockTime shouldn't be 0! (this connection is likely broken)"); } return _rpcClient!; - } catch (e) { - print("Attempt $i failed: $e"); + } on GrpcError catch (e) { + log("Attempt $i failed: $e"); + log('Caught grpc error: ${e.message}'); _rpcClient = null; + await Future.delayed(const Duration(seconds: 3)); + } catch (e) { + log("Attempt $i failed: $e"); + _rpcClient = null; + await Future.delayed(const Duration(seconds: 3)); } } throw Exception("Failed to connect after $maxRetries attempts"); @@ -61,22 +97,43 @@ class CwMweb { try { await CwMwebPlatform.instance.stop(); await cleanup(); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error stopping server: $e"); + log("Error stopping server: $e"); } } static Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { try { - return CwMwebPlatform.instance.address(scanSecret, spendPub, index); + return (await CwMwebPlatform.instance.addresses(scanSecret, spendPub, index, index + 1)) + ?.split(',') + .first; + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting address: $e"); - return null; + log("Error getting address: $e"); } + return null; + } + + static Future?> addresses( + Uint8List scanSecret, Uint8List spendPub, int fromIndex, int toIndex) async { + try { + return (await CwMwebPlatform.instance.addresses(scanSecret, spendPub, fromIndex, toIndex)) + ?.split(','); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log("Error getting addresses: $e"); + } + return null; } static Future cleanup() async { - await _clientChannel?.terminate(); + try { + await _clientChannel?.terminate(); + } catch (_) {} _rpcClient = null; _clientChannel = null; _port = null; @@ -84,51 +141,57 @@ class CwMweb { // wrappers that handle the connection issues: static Future spent(SpentRequest request) async { + log("mweb.spent() called"); try { - if (_rpcClient == null) { - await _initializeClient(); - } + _rpcClient = await stub(); return await _rpcClient!.spent(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting spent: $e"); - return SpentResponse(); + log("Error getting spent: $e"); } + return SpentResponse(); } static Future status(StatusRequest request) async { + log("mweb.status() called"); try { - if (_rpcClient == null) { - await _initializeClient(); - } + _rpcClient = await stub(); return await _rpcClient!.status(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting status: $e"); - return StatusResponse(); + log("Error getting status: $e"); } + return StatusResponse(); } static Future create(CreateRequest request) async { + log("mweb.create() called"); try { - if (_rpcClient == null) { - await _initializeClient(); - } + _rpcClient = await stub(); return await _rpcClient!.create(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting create: $e"); - return CreateResponse(); + log("Error getting create: $e"); } + return CreateResponse(); } static Future?> utxos(UtxosRequest request) async { + log("mweb.utxos() called"); try { - if (_rpcClient == null) { - await _initializeClient(); - } - // this is a stream, so we should have an effectively infinite timeout: - return _rpcClient!.utxos(request, options: CallOptions(timeout: const Duration(days: 1000 * 365))); + _rpcClient = await stub(); + final resp = _rpcClient! + .utxos(request, options: CallOptions(timeout: const Duration(days: 1000 * 365))); + log("got utxo stream"); + return resp; + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting utxos: $e"); - return null; + log("Error getting utxos: $e"); } + return null; } } diff --git a/cw_mweb/lib/cw_mweb_method_channel.dart b/cw_mweb/lib/cw_mweb_method_channel.dart index 4956dcb8d..1aeb1939e 100644 --- a/cw_mweb/lib/cw_mweb_method_channel.dart +++ b/cw_mweb/lib/cw_mweb_method_channel.dart @@ -1,3 +1,5 @@ +import 'dart:io' show Platform; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -11,6 +13,9 @@ class MethodChannelCwMweb extends CwMwebPlatform { @override Future start(String dataDir, String nodeUri) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } final result = await methodChannel.invokeMethod('start', {'dataDir': dataDir, 'nodeUri': nodeUri}); return result; @@ -18,11 +23,17 @@ class MethodChannelCwMweb extends CwMwebPlatform { @override Future stop() async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return; + } await methodChannel.invokeMethod('stop'); } @override Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } final result = await methodChannel.invokeMethod('address', { 'scanSecret': scanSecret, 'spendPub': spendPub, @@ -30,4 +41,18 @@ class MethodChannelCwMweb extends CwMwebPlatform { }); return result; } + + @override + Future addresses(Uint8List scanSecret, Uint8List spendPub, int fromIndex, int toIndex) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } + final result = await methodChannel.invokeMethod('addresses', { + 'scanSecret': scanSecret, + 'spendPub': spendPub, + 'fromIndex': fromIndex, + 'toIndex': toIndex, + }); + return result; + } } diff --git a/cw_mweb/lib/cw_mweb_platform_interface.dart b/cw_mweb/lib/cw_mweb_platform_interface.dart index d5c03a56c..6ec22bf56 100644 --- a/cw_mweb/lib/cw_mweb_platform_interface.dart +++ b/cw_mweb/lib/cw_mweb_platform_interface.dart @@ -36,4 +36,8 @@ abstract class CwMwebPlatform extends PlatformInterface { Future address(Uint8List scanSecret, Uint8List spendPub, int index) { throw UnimplementedError('address(int) has not been implemented.'); } + + Future addresses(Uint8List scanSecret, Uint8List spendPub, int fromIndex, int toIndex) { + throw UnimplementedError('addresses has not been implemented.'); + } } diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 040fc9522..f89c595c9 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -149,7 +149,7 @@ abstract class NanoWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() { + Future close({required bool shouldCleanup}) async { _client.stop(); _receiveTimer?.cancel(); } diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index ad1885d8b..dd955e344 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -117,10 +117,10 @@ packages: dependency: "direct overridden" description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.7+1" built_collection: dependency: transitive description: diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 3c3078f2e..da082e833 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -179,7 +179,7 @@ abstract class SolanaWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() { + Future close({required bool shouldCleanup}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); } diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index 9012ef528..3cd8bfc99 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -217,7 +217,7 @@ abstract class TronWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() => _transactionsUpdateTimer?.cancel(); + Future close({required bool shouldCleanup}) async => _transactionsUpdateTimer?.cancel(); @action @override diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 02d968eb3..331957d67 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -160,7 +160,7 @@ abstract class WowneroWalletBase Future? updateBalance() => null; @override - void close() async { + Future close({required bool shouldCleanup}) async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); _onTxHistoryChangeReaction?.reaction.dispose(); diff --git a/cw_wownero/lib/wownero_wallet_service.dart b/cw_wownero/lib/wownero_wallet_service.dart index 286bfccd0..6f0fafc88 100644 --- a/cw_wownero/lib/wownero_wallet_service.dart +++ b/cw_wownero/lib/wownero_wallet_service.dart @@ -134,7 +134,7 @@ class WowneroWalletService extends WalletService< if (!isValid) { await restoreOrResetWalletFiles(name); - wallet.close(); + wallet.close(shouldCleanup: false); return openWallet(name, password); } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e574aafc1..322ef6f86 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,7 +7,38 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.2) + - cw_haven (0.0.1): + - cw_haven/Boost (= 0.0.1) + - cw_haven/Haven (= 0.0.1) + - cw_haven/OpenSSL (= 0.0.1) + - cw_haven/Sodium (= 0.0.1) + - cw_shared_external + - Flutter + - cw_haven/Boost (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Haven (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/OpenSSL (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Sodium (0.0.1): + - cw_shared_external + - Flutter - cw_mweb (0.0.1): + - Flutter + - cw_shared_external (0.0.1): + - cw_shared_external/Boost (= 0.0.1) + - cw_shared_external/OpenSSL (= 0.0.1) + - cw_shared_external/Sodium (= 0.0.1) + - Flutter + - cw_shared_external/Boost (0.0.1): + - Flutter + - cw_shared_external/OpenSSL (0.0.1): + - Flutter + - cw_shared_external/Sodium (0.0.1): + - Flutter - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -96,7 +127,7 @@ PODS: - FlutterMacOS - sp_scanner (0.0.1): - Flutter - - SwiftProtobuf (1.26.0) + - SwiftProtobuf (1.27.1) - SwiftyGif (5.4.5) - Toast (4.1.1) - uni_links (0.0.1): @@ -112,7 +143,9 @@ DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift + - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) + - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -125,7 +158,6 @@ DEPENDENCIES: - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - package_info (from `.symlinks/plugins/package_info/ios`) - 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`) @@ -158,8 +190,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/barcode_scan2/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cw_haven: + :path: ".symlinks/plugins/cw_haven/ios" cw_mweb: :path: ".symlinks/plugins/cw_mweb/ios" + cw_shared_external: + :path: ".symlinks/plugins/cw_shared_external/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -184,8 +220,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/in_app_review/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" - package_info: - :path: ".symlinks/plugins/package_info/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -215,7 +249,9 @@ SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea + cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_mweb: 87af74f9659fed0c1a2cbfb44413f1070e79e3ae + cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: b22617f40038496deffba44747101255cee005b0 @@ -243,7 +279,7 @@ SPEC CHECKSUMS: share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 - SwiftProtobuf: 5e8349171e7c2f88f5b9e683cb3cb79d1dc780b3 + SwiftProtobuf: b109bd17979d7993a84da14b1e1fdd8b0ded934a SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 10cc6434d..09c75feee 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,12 +14,12 @@ 2193F104374FA2746CE8945B /* ResourceHelper.swift in Resources */ = {isa = PBXBuildFile; fileRef = 78D25C60B94E9D9E48D52E5E /* ResourceHelper.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 495FEFF9B395392FED3425DE /* TaskProtocol.swift in Resources */ = {isa = PBXBuildFile; fileRef = 0F42D8065219E0653321EE2B /* TaskProtocol.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; - 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C663361C56EBB242598F609 /* Pods_Runner.framework */; }; 525A2200C6C2A43EDC5C8FC5 /* BreezSDKConnector.swift in Resources */ = {isa = PBXBuildFile; fileRef = 1FB06A93B13D606F06B3924D /* BreezSDKConnector.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 6909E1D79C9986ADF2DE41E9 /* LnurlPayInvoice.swift in Resources */ = {isa = PBXBuildFile; fileRef = DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 724FDA327BF191BC29DCAA2E /* Constants.swift in Resources */ = {isa = PBXBuildFile; fileRef = 0CCA7ADAD6FF9185EBBB2BCA /* Constants.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 73138617307FA4F838D21D62 /* ServiceLogger.swift in Resources */ = {isa = PBXBuildFile; fileRef = F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8B1F4FCAA5EB9F3A83D32D5F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D7CD6B6020744E8FA471915D /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -50,6 +50,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 014D7E4DBCFD76DDE652A4D9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 0C400E0F25B21ABB0025E469 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 0C44A7192518EF8000B570ED /* decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = decrypt.swift; sourceTree = ""; }; 0C50DFB82BF3CB56002B0EB3 /* MoneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MoneroWallet.framework; sourceTree = ""; }; @@ -57,13 +58,11 @@ 0C9D68C8264854B60011B691 /* secRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = secRandom.swift; sourceTree = ""; }; 0CCA7ADAD6FF9185EBBB2BCA /* Constants.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Constants.swift"; sourceTree = ""; }; 0F42D8065219E0653321EE2B /* TaskProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TaskProtocol.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/TaskProtocol.swift"; sourceTree = ""; }; - 11F9FC13F9EE2A705B213FA9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1FB06A93B13D606F06B3924D /* BreezSDKConnector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDKConnector.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDKConnector.swift"; sourceTree = ""; }; + 28F61114229803070973270D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3C663361C56EBB242598F609 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 41102141140E57B1DC27FBA1 /* SDKNotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SDKNotificationService.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/SDKNotificationService.swift"; sourceTree = ""; }; 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInfo.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInfo.swift"; sourceTree = ""; }; 5AFFEBFC279AD49C00F906A4 /* wakeLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wakeLock.swift; sourceTree = ""; }; @@ -83,11 +82,12 @@ 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedeemSwap.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/RedeemSwap.swift"; sourceTree = ""; }; 9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; ABD6FCBB0F4244B090459128 /* BreezSDK.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDK.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDK.swift"; sourceTree = ""; }; - AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = ""; }; C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WowneroWallet.framework; sourceTree = ""; }; CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Mwebd.xcframework; sourceTree = ""; }; + D139E30AEB36740C21C00A9E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + D7CD6B6020744E8FA471915D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInvoice.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInvoice.swift"; sourceTree = ""; }; F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceLogger.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceLogger.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -97,8 +97,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */, CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */, + 8B1F4FCAA5EB9F3A83D32D5F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -111,7 +111,7 @@ CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */, C58D93382C00FAC6004BCF69 /* libresolv.tbd */, 0C9986A3251A932F00D566FD /* CryptoSwift.framework */, - 3C663361C56EBB242598F609 /* Pods_Runner.framework */, + D7CD6B6020744E8FA471915D /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -137,10 +137,10 @@ 84389F1A05D5860790D82820 /* Pods */ = { isa = PBXGroup; children = ( - 11F9FC13F9EE2A705B213FA9 /* Pods-Runner.debug.xcconfig */, - 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */, - AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */, 0B80439B9064C9708DDB0ADA /* breez_sdk-OnDemandResources */, + 014D7E4DBCFD76DDE652A4D9 /* Pods-Runner.debug.xcconfig */, + 28F61114229803070973270D /* Pods-Runner.release.xcconfig */, + D139E30AEB36740C21C00A9E /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -222,14 +222,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B91154210ADCED81FBF06A85 /* [CP] Check Pods Manifest.lock */, + 11278EDF4D5DB437B3FDB787 /* [CP] Check Pods Manifest.lock */, CE5E8A222BEE19C700608EA1 /* CopyFiles */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 32D0076A9969C0C38D68AF62 /* [CP] Embed Pods Frameworks */, + F6F67323547956BC4F7B67F1 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -305,21 +305,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 32D0076A9969C0C38D68AF62 /* [CP] Embed Pods Frameworks */ = { + 11278EDF4D5DB437B3FDB787 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -353,26 +358,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - B91154210ADCED81FBF06A85 /* [CP] Check Pods Manifest.lock */ = { + F6F67323547956BC4F7B67F1 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 2bed8a451..e9e83597a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -262,7 +262,14 @@ class CWBitcoin extends Bitcoin { List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; @override - List getLitecoinReceivePageOptions() => BitcoinReceivePageOption.allLitecoin; + List getLitecoinReceivePageOptions() { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return BitcoinReceivePageOption.allLitecoin + .where((element) => element != BitcoinReceivePageOption.mweb) + .toList(); + } + return BitcoinReceivePageOption.allLitecoin; + } @override BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { @@ -608,7 +615,7 @@ class CWBitcoin extends Bitcoin { final updatedOutputs = outputs.map((output) { try { - final pendingOut = pendingTx!.outputs[outputs.indexOf(output)]; + final pendingOut = pendingTx.outputs[outputs.indexOf(output)]; final updatedOutput = output; updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey) @@ -658,8 +665,8 @@ class CWBitcoin extends Bitcoin { String? getUnusedMwebAddress(Object wallet) { try { final electrumWallet = wallet as ElectrumWallet; - final walletAddresses = electrumWallet.walletAddresses as ElectrumWalletAddresses; - final mwebAddress = walletAddresses.mwebAddresses.firstWhere((element) => !element.isUsed); + final mwebAddress = + electrumWallet.walletAddresses.mwebAddresses.firstWhere((element) => !element.isUsed); return mwebAddress.address; } catch (_) { return null; diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 08e045a0d..3b88c685a 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -6,7 +6,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; @@ -20,12 +19,11 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; -import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; -import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -382,18 +380,10 @@ class CryptoBalanceWidget extends StatelessWidget { child: DashBoardRoundedCardWidget( customBorder: 30, title: S.of(context).litecoin_mweb, - subTitle: '', + subTitle: S.of(context).litecoin_mweb_description, hint: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - S.of(context).litecoin_mweb_description, - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 8), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => launchUrl( @@ -401,53 +391,64 @@ class CryptoBalanceWidget extends StatelessWidget { "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), mode: LaunchMode.externalApplication, ), - child: Center( - child: Text( - S.of(context).learn_more, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - softWrap: true, + child: Text( + S.of(context).learn_more, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, ), + softWrap: true, ), ), - SizedBox(height: 24), + SizedBox(height: 8), Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - ElevatedButton( - onPressed: () => _dismissMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), + Expanded( + child: ElevatedButton( + onPressed: () => _dismissMweb(context), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + ), + child: Text( + S.of(context).litecoin_mweb_dismiss, + style: TextStyle(color: Colors.white), ), ), - child: Text( - S.of(context).litecoin_mweb_dismiss, - style: TextStyle(color: Colors.white), - ), ), - ElevatedButton( - onPressed: () => _enableMweb(context), - child: Text(S.of(context).litecoin_enable_mweb_sync), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: () => _enableMweb(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + child: Text( + S.of(context).enable, + maxLines: 1, + ), + ), ), ], ), ], ), onTap: () => {}, - icon: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: - Theme.of(context).extension()!.pageTitleTextColor, - size: 50, + icon: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Color.fromARGB(255, 11, 70, 129), + size: 40, + ), ), ), ), @@ -505,7 +506,7 @@ class CryptoBalanceWidget extends StatelessWidget { }, )); } - dashboardViewModel.setMwebScanningActive(); + dashboardViewModel.setMwebEnabled(); } Future _dismissMweb(BuildContext context) async { @@ -850,23 +851,15 @@ class BalanceRowWidget extends StatelessWidget { padding: EdgeInsets.only(right: 16, top: 16), child: Column( children: [ - CakeImageWidget( - imageUrl: 'assets/images/mweb_logo.png', - height: 40, - width: 40, - displayOnError: Container( - height: 30.0, - width: 30.0, - child: Center( - child: Text( - currency.title.substring(0, min(currency.title.length, 2)), - style: TextStyle(fontSize: 11), - ), - ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey.shade400, - ), + Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Color.fromARGB(255, 11, 70, 129), + size: 40, ), ), const SizedBox(height: 10), diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index a59bfd2fa..6d8379b29 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -97,14 +97,13 @@ class MenuWidgetState extends State { @override Widget build(BuildContext context) { - List items = SettingActions.all; + List items = List.of(SettingActions.all); if (!widget.dashboardViewModel.hasSilentPayments) { items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings); } - // if (!widget.dashboardViewModel.hasMweb) { - // itemCount--; - // items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings); - // } + if (!widget.dashboardViewModel.hasMweb) { + items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings); + } int itemCount = items.length; moneroIcon = Image.asset('assets/images/monero_menu.png', @@ -191,11 +190,6 @@ class MenuWidgetState extends State { final item = items[index]; - if (!widget.dashboardViewModel.hasMweb && - item.name(context) == S.of(context).litecoin_mweb_settings) { - return const SizedBox(); - } - final isLastTile = index == itemCount - 1; return SettingActionButton( diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 2acf6138e..6b62435d0 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -135,10 +135,6 @@ class RootState extends State with WidgetsBindingObserver { setState(() => _setInactive(true)); } - if (widget.appStore.wallet?.type == WalletType.litecoin) { - widget.appStore.wallet?.stopSync(); - } - break; case AppLifecycleState.resumed: widget.authService.requireAuth().then((value) { @@ -148,9 +144,6 @@ class RootState extends State with WidgetsBindingObserver { }); } }); - if (widget.appStore.wallet?.type == WalletType.litecoin) { - widget.appStore.wallet?.startSync(); - } break; default: break; diff --git a/lib/src/screens/settings/mweb_settings.dart b/lib/src/screens/settings/mweb_settings.dart index 88dc00f7c..722ffa9aa 100644 --- a/lib/src/screens/settings/mweb_settings.dart +++ b/lib/src/screens/settings/mweb_settings.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -33,9 +32,9 @@ class MwebSettingsPage extends BasePage { ), SettingsSwitcherCell( title: S.current.litecoin_mweb_always_scan, - value: _mwebSettingsViewModel.mwebAlwaysScan, + value: _mwebSettingsViewModel.mwebEnabled, onValueChange: (_, bool value) { - _mwebSettingsViewModel.setMwebAlwaysScan(value); + _mwebSettingsViewModel.setMwebEnabled(value); }, ), SettingsCellWithArrow( diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index 80d07962f..b9af97f32 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -31,7 +31,6 @@ class SettingActions { walletSettingAction, addressBookSettingAction, silentPaymentsSettingAction, - litecoinMwebSettingAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -50,7 +49,7 @@ class SettingActions { static SettingActions litecoinMwebSettingAction = SettingActions._( name: (context) => S.of(context).litecoin_mweb_settings, - image: 'assets/images/bitcoin_menu.png', + image: 'assets/images/litecoin_menu.png', onTap: (BuildContext context) { Navigator.pop(context); Navigator.of(context).pushNamed(Routes.mwebSettings); diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index 7d61abfc5..cd8881633 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -37,7 +37,8 @@ abstract class AppStoreBase with Store { @action Future changeCurrentWallet( WalletBase, TransactionInfo> wallet) async { - this.wallet?.close(); + bool changingToSameWalletType = this.wallet?.type == wallet.type; + this.wallet?.close(shouldCleanup: !changingToSameWalletType); this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 083c31302..56a5935c9 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -31,6 +32,7 @@ class BalanceRecord { required this.fiatSecondAdditionalBalance, required this.asset, required this.formattedAssetTitle}); + final String fiatAdditionalBalance; final String fiatAvailableBalance; final String fiatFrozenBalance; @@ -53,7 +55,22 @@ abstract class BalanceViewModelBase with Store { : isReversing = false, isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard, wallet = appStore.wallet! { - reaction((_) => appStore.wallet, _onWalletChange); + reaction((_) => appStore.wallet, (wallet) { + _onWalletChange(wallet); + _checkMweb(); + }); + + _checkMweb(); + + reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { + _checkMweb(); + }); + } + + void _checkMweb() { + if (wallet.type == WalletType.litecoin) { + mwebEnabled = bitcoin!.getMwebEnabled(wallet); + } } final AppStore appStore; @@ -336,14 +353,19 @@ abstract class BalanceViewModelBase with Store { }); } + @observable + bool mwebEnabled = false; + @computed bool get hasAdditionalBalance => _hasAdditionalBalanceForWalletType(wallet.type); @computed - bool get hasSecondAdditionalBalance => _hasSecondAdditionalBalanceForWalletType(wallet.type); + bool get hasSecondAdditionalBalance => + mwebEnabled && _hasSecondAdditionalBalanceForWalletType(wallet.type); @computed - bool get hasSecondAvailableBalance => _hasSecondAvailableBalanceForWalletType(wallet.type); + bool get hasSecondAvailableBalance => + mwebEnabled && _hasSecondAvailableBalanceForWalletType(wallet.type); bool _hasAdditionalBalanceForWalletType(WalletType type) { switch (type) { @@ -358,15 +380,16 @@ abstract class BalanceViewModelBase with Store { } bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin && settingsStore.mwebAlwaysScan) { - // if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) - return true; + if (wallet.type == WalletType.litecoin) { + if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) { + return true; + } } return false; } bool _hasSecondAvailableBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin && settingsStore.mwebAlwaysScan) { + if (wallet.type == WalletType.litecoin) { return true; } return false; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 77c60bd63..26bc5ceb9 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:io' show Platform; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; @@ -222,7 +224,10 @@ abstract class DashboardViewModelBase with Store { // subname = nano!.getCurrentAccount(_wallet).label; // } - reaction((_) => appStore.wallet, _onWalletChange); + reaction((_) => appStore.wallet, (wallet) { + _onWalletChange(wallet); + _checkMweb(); + }); connectMapToListWithTransform( appStore.wallet!.transactionHistory.transactions, @@ -256,16 +261,16 @@ abstract class DashboardViewModelBase with Store { }); } + _checkMweb(); + reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { + _checkMweb(); + }); + } + + void _checkMweb() { if (hasMweb) { - mwebScanningActive = bitcoin!.getMwebEnabled(wallet); - settingsStore.mwebEnabled = mwebScanningActive; - reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) { - if (alwaysScan) { - mwebScanningActive = true; - } else { - mwebScanningActive = false; - } - }); + mwebEnabled = bitcoin!.getMwebEnabled(wallet); + balanceViewModel.mwebEnabled = mwebEnabled; } } @@ -430,34 +435,36 @@ abstract class DashboardViewModelBase with Store { } @computed - bool get hasMweb => wallet.type == WalletType.litecoin; + bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid); @computed - bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebScanningActive; + bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled; @observable - bool mwebScanningActive = false; + bool mwebEnabled = false; @computed bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; @action - void setMwebScanningActive() { + void setMwebEnabled() { if (!hasMweb) { return; } settingsStore.hasEnabledMwebBefore = true; - mwebScanningActive = true; + mwebEnabled = true; bitcoin!.setMwebEnabled(wallet, true); + balanceViewModel.mwebEnabled = true; settingsStore.mwebAlwaysScan = true; } @action void dismissMweb() { settingsStore.mwebCardDisplay = false; + balanceViewModel.mwebEnabled = false; settingsStore.mwebAlwaysScan = false; - mwebScanningActive = false; + mwebEnabled = false; bitcoin!.setMwebEnabled(wallet, false); } diff --git a/lib/view_model/settings/mweb_settings_view_model.dart b/lib/view_model/settings/mweb_settings_view_model.dart index 343947d00..c6370e23f 100644 --- a/lib/view_model/settings/mweb_settings_view_model.dart +++ b/lib/view_model/settings/mweb_settings_view_model.dart @@ -8,7 +8,10 @@ part 'mweb_settings_view_model.g.dart'; class MwebSettingsViewModel = MwebSettingsViewModelBase with _$MwebSettingsViewModel; abstract class MwebSettingsViewModelBase with Store { - MwebSettingsViewModelBase(this._settingsStore, this._wallet); + MwebSettingsViewModelBase(this._settingsStore, this._wallet) { + mwebEnabled = bitcoin!.getMwebEnabled(_wallet); + _settingsStore.mwebAlwaysScan = mwebEnabled; + } final SettingsStore _settingsStore; final WalletBase _wallet; @@ -16,8 +19,8 @@ abstract class MwebSettingsViewModelBase with Store { @computed bool get mwebCardDisplay => _settingsStore.mwebCardDisplay; - @computed - bool get mwebAlwaysScan => _settingsStore.mwebAlwaysScan; + @observable + late bool mwebEnabled; @action void setMwebCardDisplay(bool value) { @@ -25,8 +28,9 @@ abstract class MwebSettingsViewModelBase with Store { } @action - void setMwebAlwaysScan(bool value) { - _settingsStore.mwebAlwaysScan = value; + void setMwebEnabled(bool value) { + mwebEnabled = value; bitcoin!.setMwebEnabled(_wallet, value); + _settingsStore.mwebAlwaysScan = value; } } diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 13c1a5661..f31f879a1 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -188,36 +188,38 @@ abstract class WalletCreationVMBase with Store { } } - Future> getDerivationInfoFromQRCredentials(RestoredWallet restoreWallet) async { - var list = []; - final walletType = restoreWallet.type; - var appStore = getIt.get(); - var node = appStore.settingsStore.getCurrentNode(walletType); + Future> getDerivationInfoFromQRCredentials( + RestoredWallet restoreWallet) async { + var list = []; + final walletType = restoreWallet.type; + var appStore = getIt.get(); + var node = appStore.settingsStore.getCurrentNode(walletType); - switch (walletType) { - case WalletType.bitcoin: - case WalletType.litecoin: - return bitcoin!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - passphrase: restoreWallet.passphrase, - ); - case WalletType.nano: - return nanoUtil!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - default: - break; - } - return list; + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + return bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; } + return list; + } WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); Future process(WalletCredentials credentials) => throw UnimplementedError(); - Future getWalletCredentialsFromQRCredentials(RestoredWallet restoreWallet) async => + Future getWalletCredentialsFromQRCredentials( + RestoredWallet restoreWallet) async => throw UnimplementedError(); Future processFromRestoredWallet( diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 4a4fdcb19..0ae6b75a5 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -70,6 +70,8 @@ abstract class WalletListViewModelBase with Store { @action Future loadWallet(WalletListItem walletItem) async { + // bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type; + // await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType); final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name); await _appStore.changeCurrentWallet(wallet); } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 8951a2dd1..d6199d028 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,6 +2,8 @@ PODS: - connectivity_plus (0.0.1): - FlutterMacOS - ReachabilitySwift + - cw_mweb (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - devicelocale (0.0.1): @@ -37,6 +39,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) @@ -60,6 +63,8 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + cw_mweb: + :path: Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: @@ -91,6 +96,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d @@ -105,7 +111,7 @@ SPEC CHECKSUMS: share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: 269d96e0ec3173e69156be7239b95182be3b8303 - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 PODFILE CHECKSUM: 65ec1541137fb5b35d00490dec1bb48d4d9586bb diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index d55faad76..3797334e9 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -235,6 +235,7 @@ "edit_token": "تحرير الرمز المميز", "electrum_address_disclaimer": "نقوم بإنشاء عناوين جديدة في كل مرة تستخدم فيها عنوانًا ، لكن العناوين السابقة تستمر في العمل", "email_address": "عنوان البريد الالكترونى", + "enable": "يُمكَِن", "enable_mempool_api": "MEMPOOL API للحصول على رسوم وتواريخ دقيقة", "enable_replace_by_fee": "تمكين الاستبدال", "enable_silent_payments_scanning": "ابدأ في مسح المدفوعات الصامتة ، حتى يتم الوصول إلى الطرف", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index dd5c9a260..1d674530b 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -235,6 +235,7 @@ "edit_token": "Редактиране на токена", "electrum_address_disclaimer": "Нови адреси се генерират всеки път, когато използвате този, но и предишните продължават да работят", "email_address": "Имейл адрес", + "enable": "Активиране", "enable_mempool_api": "Mempool API за точни такси и дати", "enable_replace_by_fee": "Активиране на замяна по забрана", "enable_silent_payments_scanning": "Започнете да сканирате безшумните плащания, докато се достигне съветът", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 65ef50a71..b97c1e9f8 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -235,6 +235,7 @@ "edit_token": "Upravit token", "electrum_address_disclaimer": "Po každém použití je generována nová adresa, ale předchozí adresy také stále fungují", "email_address": "E-mailová adresa", + "enable": "Umožnit", "enable_mempool_api": "Mempool API pro přesné poplatky a data", "enable_replace_by_fee": "Povolit výměnu podle poplatku", "enable_silent_payments_scanning": "Začněte skenovat tiché platby, dokud není dosaženo špičky", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 194b1d05b..80c33a3b7 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -235,6 +235,7 @@ "edit_token": "Token bearbeiten", "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", "email_address": "E-Mail-Adresse", + "enable": "Aktivieren", "enable_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 stille Zahlungen, bis die Spitze erreicht ist", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index c79719641..b1547be88 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -235,6 +235,7 @@ "edit_token": "Edit token", "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", "email_address": "Email Address", + "enable": "Enable", "enable_mempool_api": "Mempool API for accurate fees and dates", "enable_replace_by_fee": "Enable Replace-By-Fee", "enable_silent_payments_scanning": "Start scanning for transactions sent to your Silent Payment address.", @@ -940,4 +941,4 @@ "you_will_get": "Convert to", "you_will_send": "Convert from", "yy": "YY" -} +} \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index ced8d9a13..c4e6bb21f 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -235,6 +235,7 @@ "edit_token": "Editar token", "electrum_address_disclaimer": "Generamos nuevas direcciones cada vez que usa una, pero las direcciones anteriores siguen funcionando", "email_address": "Dirección de correo electrónico", + "enable": "Permitir", "enable_mempool_api": "API de Mempool para tarifas y fechas precisas", "enable_replace_by_fee": "Habilitar reemplazar por tarea", "enable_silent_payments_scanning": "Comience a escanear pagos silenciosos, hasta que se alcance la punta", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 1f8567b31..8508dda99 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -235,6 +235,7 @@ "edit_token": "Modifier le token", "electrum_address_disclaimer": "Nous générons de nouvelles adresses à chaque fois que vous en utilisez une, mais les adresses précédentes continuent à fonctionner", "email_address": "Adresse e-mail", + "enable": "Activer", "enable_mempool_api": "API Mempool pour les frais et dates précis", "enable_replace_by_fee": "Activer Remplace-by-Fee", "enable_silent_payments_scanning": "Commencez à scanner les paiements silencieux, jusqu'à ce que la pointe soit atteinte", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 1e5b5d8e3..b25cc14f5 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -235,6 +235,7 @@ "edit_token": "Gyara alamar", "electrum_address_disclaimer": "Muna samar da sababbin adireshi duk lokacin da kuka yi amfani da ɗaya, amma adiresoshin da suka gabata suna ci gaba da aiki", "email_address": "Adireshin i-mel", + "enable": "Ba dama", "enable_mempool_api": "Mampool API don ingantattun kudade da kwanakin", "enable_replace_by_fee": "Ba da damar maye gurbin-by-kudin", "enable_silent_payments_scanning": "Fara bincika biya na shiru, har sai tip ɗin ya kai", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 07d8311ef..c00184218 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -235,6 +235,7 @@ "edit_token": "टोकन संपादित करें", "electrum_address_disclaimer": "हर बार जब आप एक का उपयोग करते हैं तो हम नए पते उत्पन्न करते हैं, लेकिन पिछले पते काम करना जारी रखते हैं", "email_address": "ईमेल पता", + "enable": "सक्षम", "enable_mempool_api": "सटीक शुल्क और तिथियों के लिए मेमपूल एपीआई", "enable_replace_by_fee": "प्रतिस्थापित-दर-शुल्क सक्षम करें", "enable_silent_payments_scanning": "मूक भुगतान स्कैनिंग सक्षम करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 7969716de..030d6f49b 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -235,6 +235,7 @@ "edit_token": "Uredi token", "electrum_address_disclaimer": "Minden egyes alkalommal új címeket generálunk, de a korábbi címek továbbra is működnek", "email_address": "Adresa e-pošte", + "enable": "Omogućiti", "enable_mempool_api": "Mempool API za točne naknade i datume", "enable_replace_by_fee": "Omogući zamjenu", "enable_silent_payments_scanning": "Započnite skeniranje tihih plaćanja, dok se ne postigne savjet", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index aa58edd64..1869001a7 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -235,6 +235,7 @@ "edit_token": "Փոփոխել տոկեն", "electrum_address_disclaimer": "Մենք ստեղծում ենք նոր հասցե ամեն անգամ, երբ դուք օգտագործում եք այն, բայց նախորդ հասցեները շարունակում են աշխատել", "email_address": "Էլ. փոստի հասցե", + "enable": "Միացնել", "enable_mempool_api": "Mempool API ճշգրիտ վճարների եւ ամսաթվերի համար", "enable_replace_by_fee": "Միացնել փոխարինումը միջնորդավճարով", "enable_silent_payments_scanning": "Միացնել Լուռ Վճարումների սկանավորումը", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 867040df5..b215fa148 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -235,6 +235,7 @@ "edit_token": "Mengedit token", "electrum_address_disclaimer": "Kami menghasilkan alamat baru setiap kali Anda menggunakan satu, tetapi alamat sebelumnya tetap berfungsi", "email_address": "Alamat Email", + "enable": "Memungkinkan", "enable_mempool_api": "API Mempool untuk biaya dan tanggal yang akurat", "enable_replace_by_fee": "Aktifkan ganti-by-fee", "enable_silent_payments_scanning": "Mulailah memindai pembayaran diam, sampai ujung tercapai", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 3b20d0410..14963f58b 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -236,6 +236,7 @@ "edit_token": "Modifica token", "electrum_address_disclaimer": "Generiamo nuovi indirizzi ogni volta che ne utilizzi uno, ma gli indirizzi precedenti continuano a funzionare", "email_address": "Indirizzo e-mail", + "enable": "Abilitare", "enable_mempool_api": "API di Mempool per commissioni e date accurate", "enable_replace_by_fee": "Abilita sostituzione per fee", "enable_silent_payments_scanning": "Inizia a scansionare i pagamenti silenziosi, fino a raggiungere la punta", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 0449a8749..af61ab1f9 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -235,6 +235,7 @@ "edit_token": "トークンの編集", "electrum_address_disclaimer": "使用するたびに新しいアドレスが生成されますが、以前のアドレスは引き続き機能します", "email_address": "メールアドレス", + "enable": "有効にする", "enable_mempool_api": "正確な料金と日付のMempool API", "enable_replace_by_fee": "交換ごとに有効にします", "enable_silent_payments_scanning": "先端に達するまで、サイレント決済のスキャンを開始します", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index ccda2dbbc..dd19af792 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -235,6 +235,7 @@ "edit_token": "토큰 편집", "electrum_address_disclaimer": "사용할 때마다 새 주소가 생성되지만 이전 주소는 계속 작동합니다.", "email_address": "이메일 주소", + "enable": "할 수 있게 하다", "enable_mempool_api": "정확한 수수료 및 날짜에 대한 Mempool API", "enable_replace_by_fee": "대체별로 활성화하십시오", "enable_silent_payments_scanning": "팁에 도달 할 때까지 사일런트 지불을 스캔하기 시작합니다.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index b2c6386f0..8ce050339 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -235,6 +235,7 @@ "edit_token": "တိုကင်ကို တည်းဖြတ်ပါ။", "electrum_address_disclaimer": "သင်အသုံးပြုသည့်အချိန်တိုင်းတွင် ကျွန်ုပ်တို့သည် လိပ်စာအသစ်များကို ထုတ်ပေးသော်လည်း ယခင်လိပ်စာများသည် ဆက်လက်အလုပ်လုပ်နေပါသည်။", "email_address": "အီးမေးလ်လိပ်စာ", + "enable": "စွမ်းဆောင်နိုင်စေ", "enable_mempool_api": "Mempool API တိကျသောအခကြေးငွေနှင့်ရက်စွဲများအတွက်", "enable_replace_by_fee": "အစားထိုး - by- အခကြေးငွေ enable", "enable_silent_payments_scanning": "အစွန်အဖျားသို့ရောက်ရှိသည်အထိအသံတိတ်ငွေပေးချေမှုကိုစကင်ဖတ်စစ်ဆေးပါ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 12315cd3c..6b1dd29c5 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -235,6 +235,7 @@ "edit_token": "Token bewerken", "electrum_address_disclaimer": "We genereren nieuwe adressen elke keer dat u er een gebruikt, maar eerdere adressen blijven werken", "email_address": "E-mailadres", + "enable": "Inschakelen", "enable_mempool_api": "Mempool API voor nauwkeurige kosten en datums", "enable_replace_by_fee": "Schakel vervangen door een fee", "enable_silent_payments_scanning": "Begin met het scannen van stille betalingen, totdat de tip is bereikt", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index accb31d46..4bf323c1c 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -235,6 +235,7 @@ "edit_token": "Edytuj token", "electrum_address_disclaimer": "Za każdym razem, gdy wykorzystasz adres, dla wiekszej prywatności generujemy nowy, ale poprzednie adresy nadal działają, i moga odbierać środki", "email_address": "Adres e-mail", + "enable": "Włączać", "enable_mempool_api": "Mempool API dla dokładnych opłat i dat", "enable_replace_by_fee": "Włącz wymianę po lewej", "enable_silent_payments_scanning": "Zacznij skanować ciche płatności, aż do osiągnięcia wskazówki", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 294a6bf1b..603dd75b7 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -235,6 +235,7 @@ "edit_token": "Editar símbolo", "electrum_address_disclaimer": "Geramos novos endereços cada vez que você usa um, mas os endereços anteriores continuam funcionando", "email_address": "Endereço de e-mail", + "enable": "Habilitar", "enable_mempool_api": "Mempool API para taxas e datas precisas", "enable_replace_by_fee": "Habilite substituir por taxa", "enable_silent_payments_scanning": "Comece a escanear pagamentos silenciosos, até que o topo seja alcançada", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1c3a23d67..43d912ffc 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -235,6 +235,7 @@ "edit_token": "Изменить токен", "electrum_address_disclaimer": "Мы генерируем новые адреса каждый раз, когда вы их используете, но предыдущие адреса продолжают работать.", "email_address": "Адрес электронной почты", + "enable": "Давать возможность", "enable_mempool_api": "Mempool API за точные сборы и даты", "enable_replace_by_fee": "Включить замену за пикой", "enable_silent_payments_scanning": "Начните сканировать безмолвные платежи, пока не будет достигнут наконечник", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index d1328257d..c02c7cc33 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -235,6 +235,7 @@ "edit_token": "แก้ไขโทเค็น", "electrum_address_disclaimer": "เราสร้างที่อยู่ใหม่ทุกครั้งที่คุณใช้หนึ่งอย่าง แต่ที่อยู่เก่ายังสามารถใช้ได้ต่อไป", "email_address": "ที่อยู่อีเมล", + "enable": "เปิดใช้งาน", "enable_mempool_api": "Mempool API สำหรับค่าธรรมเนียมและวันที่ที่ถูกต้อง", "enable_replace_by_fee": "เปิดใช้งานการเปลี่ยนโดยค่าธรรมเนียม", "enable_silent_payments_scanning": "เริ่มสแกนการชำระเงินแบบเงียบจนกว่าจะถึงปลาย", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index ca3a48478..99d22bb1e 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -235,6 +235,7 @@ "edit_token": "I-edit ang token", "electrum_address_disclaimer": "Bumubuo kami ng mga bagong address sa tuwing gagamit ka ng isa, ngunit ang mga nakaraang address ay patuloy na gumagana", "email_address": "Email Address", + "enable": "Paganahin", "enable_mempool_api": "Mempool API para sa tumpak na bayad at mga petsa", "enable_replace_by_fee": "Paganahin ang Replace-By-Fee", "enable_silent_payments_scanning": "Simulan ang pag -scan ng tahimik na pagbabayad, hanggang sa maabot ang tip", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index dac8c1c81..86239cd77 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -235,6 +235,7 @@ "edit_token": "Belirteci düzenle", "electrum_address_disclaimer": "Adresini her kullandığında yeni adres oluşturuyoruz, ancak önceki adresler de çalışmaya devam eder", "email_address": "E-posta Adresi", + "enable": "Olanak vermek", "enable_mempool_api": "Doğru ücretler ve tarihler için Mempool API'si", "enable_replace_by_fee": "Farklı Değiştir'i Etkinleştir", "enable_silent_payments_scanning": "Bahşiş ulaşılıncaya kadar sessiz ödemeleri taramaya başlayın", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ba6c6516d..2d049e12e 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -235,6 +235,7 @@ "edit_token": "Редагувати маркер", "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати", "email_address": "Адреса електронної пошти", + "enable": "Ввімкнути", "enable_mempool_api": "API Mempool для точних зборів та дат", "enable_replace_by_fee": "Увімкнути заміну з комісією", "enable_silent_payments_scanning": "Почніть сканувати мовчазні платежі, поки не буде досягнуто наконечника", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 3e68a5fb4..f080c23a5 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -235,6 +235,7 @@ "edit_token": "ٹوکن میں ترمیم کریں۔", "electrum_address_disclaimer": "جب بھی آپ ایک کا استعمال کرتے ہیں تو ہم نئے پتے تیار کرتے ہیں، لیکن پچھلے پتے کام کرتے رہتے ہیں۔", "email_address": "ای میل اڈریس", + "enable": "قابل بنائیں", "enable_mempool_api": "درست فیسوں اور تاریخوں کے لئے میمپول API", "enable_replace_by_fee": "فی فیس کو تبدیل کریں", "enable_silent_payments_scanning": "خاموش ادائیگیوں کو اسکین کرنا شروع کریں ، جب تک کہ نوک نہ پہنچ جائے", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 159a127d0..63db7ce88 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -236,6 +236,7 @@ "edit_token": "Chỉnh sửa token", "electrum_address_disclaimer": "Chúng tôi tạo địa chỉ mới mỗi khi bạn sử dụng, nhưng các địa chỉ cũ vẫn tiếp tục hoạt động", "email_address": "Địa chỉ Email", + "enable": "Cho phép", "enable_mempool_api": "API Mempool cho các khoản phí và ngày chính xác", "enable_replace_by_fee": "Bật Thay thế Bằng Phí", "enable_silent_payments_scanning": "Bật quét thanh toán im lặng", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index a12774d5d..16a887a39 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -236,6 +236,7 @@ "edit_token": "Ṣatunkọ àmi", "electrum_address_disclaimer": "A dá àwọn àdírẹ́sì títun ní gbogbo àwọn ìgbà t'ẹ́ lo ó kan ṣùgbọ́n ẹ lè tẹ̀síwájú lo àwọn àdírẹ́sì tẹ́lẹ̀tẹ́lẹ̀.", "email_address": "Àdírẹ́sì ímeèlì", + "enable": "Mu ṣiṣẹ", "enable_mempool_api": "Mempool API fun awọn owo deede ati awọn ọjọ", "enable_replace_by_fee": "Mu ki o rọpo", "enable_silent_payments_scanning": "Bẹrẹ awọn sisanwo ipalọlọ, titi ti o fi de opin", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 8f8e8cd13..9398e2408 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -235,6 +235,7 @@ "edit_token": "编辑令牌", "electrum_address_disclaimer": "每次您使用一个地址时,我们都会生成新地址,但之前的地址仍然有效", "email_address": "电子邮件地址", + "enable": "使能够", "enable_mempool_api": "Mempool API获得准确的费用和日期", "enable_replace_by_fee": "启用by-Fee替换", "enable_silent_payments_scanning": "开始扫描无声付款,直到达到提示", diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index a60077e82..428891b2c 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.5" -MONERO_COM_BUILD_NUMBER=100 +MONERO_COM_VERSION="1.17.0" +MONERO_COM_BUILD_NUMBER=102 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.5" -CAKEWALLET_BUILD_NUMBER=227 +CAKEWALLET_VERSION="4.20.0" +CAKEWALLET_BUILD_NUMBER=230 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/android/build_mwebd.sh b/scripts/android/build_mwebd.sh index 90dbc4c20..4434e30f1 100755 --- a/scripts/android/build_mwebd.sh +++ b/scripts/android/build_mwebd.sh @@ -13,7 +13,7 @@ fi # build mwebd: git clone https://github.com/ltcmweb/mwebd cd mwebd -git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935 +git reset --hard 555349415f76a42ec5c76152b64c4ab9aabc448f gomobile bind -target=android -androidapi 21 . mkdir -p ../../../cw_mweb/android/libs/ mv ./mwebd.aar $_ diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 67375c914..2232491a6 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -22,7 +22,7 @@ cp -rf ./ios/Runner/InfoBase.plist ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLName string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes array" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes: string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist -sed -i '' "s/\${PRODUCT_BUNDLE_IDENTIFIER}/${APP_IOS_BUNDLE_ID}/g" ./ios/Runner.xcodeproj/project.pbxproj +sed -i '' "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_IOS_BUNDLE_ID;/g" ./ios/Runner.xcodeproj/project.pbxproj CONFIG_ARGS="" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 9df4b685e..d976f07fd 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.16.5" -MONERO_COM_BUILD_NUMBER=98 +MONERO_COM_VERSION="1.17.0" +MONERO_COM_BUILD_NUMBER=100 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.5" -CAKEWALLET_BUILD_NUMBER=266 +CAKEWALLET_VERSION="4.20.0" +CAKEWALLET_BUILD_NUMBER=269 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/ios/build_mwebd.sh b/scripts/ios/build_mwebd.sh index f0fa64605..e13c4931c 100755 --- a/scripts/ios/build_mwebd.sh +++ b/scripts/ios/build_mwebd.sh @@ -12,7 +12,7 @@ fi # build mwebd: git clone https://github.com/ltcmweb/mwebd cd mwebd -git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935 +git reset --hard 555349415f76a42ec5c76152b64c4ab9aabc448f gomobile bind -target=ios . mv -fn ./Mwebd.xcframework ../../../ios/ # cleanup: diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 1c89c6e8a..aaeb2bc2a 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.9.5" -CAKEWALLET_BUILD_NUMBER=33 +CAKEWALLET_VERSION="1.10.0" +CAKEWALLET_BUILD_NUMBER=35 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index b8785a9be..92a8636bd 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -28,7 +28,7 @@ sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/DebugProfile sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Release.entitlements sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Runner.entitlements sed -i '' "s/\${PRODUCT_NAME}/${APP_MACOS_NAME}/g" ./macos/Runner/Configs/AppInfo.xcconfig -sed -i '' "s/\${PRODUCT_BUNDLE_IDENTIFIER}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Configs/AppInfo.xcconfig +sed -i '' "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_MACOS_BUNDLE_ID;/g" ./macos/Runner/Configs/AppInfo.xcconfig CONFIG_ARGS="" case $APP_MACOS_TYPE in diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index a6afac2cb..af2dc1322 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.6.5" -MONERO_COM_BUILD_NUMBER=31 +MONERO_COM_VERSION="1.7.0" +MONERO_COM_BUILD_NUMBER=33 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.12.5" -CAKEWALLET_BUILD_NUMBER=88 +CAKEWALLET_VERSION="1.13.0" +CAKEWALLET_BUILD_NUMBER=90 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index bb13f49ef..33cd11292 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.0.6" +#define MyAppVersion "0.1.0" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" diff --git a/tool/configure.dart b/tool/configure.dart index 1e2741721..eb41c5341 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -75,6 +75,7 @@ Future main(List args) async { Future generateBitcoin(bool hasImplementation) async { final outputFile = File(bitcoinOutputPath); const bitcoinCommonHeaders = """ +import 'dart:io' show Platform; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; From ebe8c65407774c49547c2ba86f6ea83c260a8919 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 15 Oct 2024 00:28:38 +0300 Subject: [PATCH 120/142] Peg in and peg out flow (#1745) * version 4.20.0 * update build numbers * UI updates and script fix for ios bundle identifier * disable mweb for desktop * change hardcoded ltc server ip address electrum connection enhancement * additional logging and minor fixes * additional logging and minor fixes * addresses pt.1 * logs of fixes and experimental changes, close wallet before opening next * save * fix icon * fixes * [skip ci] updates * [skip ci] updates * updates * minor optimizations * fix for when switching between wallets * [skip ci] updates * [skip ci] updates * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * mobx * mostly logging * stream fix pt.1 [skip ci] * updates * some fixes and enhancements * [skip ci] minor * potential partial fix for streamsink closed * fix stream sink closed errors * fix mweb logo colors * add initial whitelisting for coin types on send screen * MWEB enhancements 2.0 (#1735) * additional logging and minor fixes * additional logging and minor fixes * addresses pt.1 * Allow Wallet Group Names to be the same as Wallet Names (#1730) * fix: Issues with imaging * fix: Allow group names to be the same as wallet names * fix: Bug with wallet grouping when a wallet is minimized * fix: Bug with wallet grouping when a wallet is minimized * logs of fixes and experimental changes, close wallet before opening next * save * fix icon * fixes * [skip ci] updates * [skip ci] updates * updates * minor optimizations * fix for when switching between wallets * [skip ci] updates * [skip ci] updates * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * mobx * mostly logging * stream fix pt.1 [skip ci] * updates * some fixes and enhancements * [skip ci] minor * potential partial fix for streamsink closed * fix stream sink closed errors * fix mweb logo colors * save * minor enhancements [skip ci] * save * experimental * minor * minor [skip ci] --------- Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: Omar Hatem * fix menu list removing from original list * handle pegin and pegout * fix text color * fix import * pegin/out button ui updates * update spacing + tx creation fix * add correct args for link view model [skip ci] --------- Co-authored-by: Matthew Fosse Co-authored-by: fossephate Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> --- .../lib/bitcoin_transaction_credentials.dart | 4 +- cw_bitcoin/lib/electrum_wallet.dart | 28 +- cw_bitcoin/lib/litecoin_wallet.dart | 7 +- cw_bitcoin/pubspec.lock | 28 +- cw_core/lib/unspent_coin_type.dart | 1 + lib/bitcoin/cw_bitcoin.dart | 60 ++- lib/di.dart | 27 +- lib/router.dart | 24 +- .../screens/dashboard/pages/balance_page.dart | 444 +++++++++++------- lib/src/screens/send/widgets/send_card.dart | 6 +- lib/view_model/link_view_model.dart | 5 +- lib/view_model/send/send_view_model.dart | 25 +- .../unspent_coins_list_view_model.dart | 12 +- tool/configure.dart | 10 +- 14 files changed, 428 insertions(+), 253 deletions(-) create mode 100644 cw_core/lib/unspent_coin_type.dart diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart index bda7c39ae..01e905fb0 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart @@ -1,11 +1,13 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; +import 'package:cw_core/unspent_coin_type.dart'; class BitcoinTransactionCredentials { BitcoinTransactionCredentials(this.outputs, - {required this.priority, this.feeRate}); + {required this.priority, this.feeRate, this.coinTypeToSpendFrom = UnspentCoinType.any}); final List outputs; final BitcoinTransactionPriority? priority; final int? feeRate; + final UnspentCoinType coinTypeToSpendFrom; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 97897046c..ceaa2f088 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -37,6 +37,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -584,6 +585,7 @@ abstract class ElectrumWalletBase required int credentialsAmount, required bool paysToSilentPayment, int? inputsCount, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { List utxos = []; List vinOutpoints = []; @@ -594,7 +596,20 @@ abstract class ElectrumWalletBase bool spendsUnconfirmedTX = false; int leftAmount = credentialsAmount; - final availableInputs = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList(); + final availableInputs = unspentCoins.where((utx) { + if (!utx.isSending || utx.isFrozen) { + return false; + } + + switch (coinTypeToSpendFrom) { + case UnspentCoinType.mweb: + return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb; + case UnspentCoinType.nonMweb: + return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.any: + return true; + } + }).toList(); final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); for (int i = 0; i < availableInputs.length; i++) { @@ -701,11 +716,13 @@ abstract class ElectrumWalletBase String? memo, int credentialsAmount = 0, bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = _createUTXOS( sendAll: true, credentialsAmount: credentialsAmount, paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); int fee = await calcFee( @@ -772,12 +789,14 @@ abstract class ElectrumWalletBase String? memo, bool? useUnconfirmed, bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = _createUTXOS( sendAll: false, credentialsAmount: credentialsAmount, inputsCount: inputsCount, paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length; @@ -797,6 +816,7 @@ abstract class ElectrumWalletBase inputsCount: utxoDetails.utxos.length + 1, memo: memo, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } @@ -855,6 +875,7 @@ abstract class ElectrumWalletBase inputsCount: utxoDetails.utxos.length + 1, memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } @@ -862,6 +883,7 @@ abstract class ElectrumWalletBase outputs, feeRate, memo: memo, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); if (estimatedSendAll.amount == credentialsAmount) { @@ -900,6 +922,7 @@ abstract class ElectrumWalletBase memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } } @@ -957,6 +980,7 @@ abstract class ElectrumWalletBase final hasMultiDestination = transactionCredentials.outputs.length > 1; final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; final memo = transactionCredentials.outputs.first.memo; + final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; int credentialsAmount = 0; bool hasSilentPayment = false; @@ -1012,6 +1036,7 @@ abstract class ElectrumWalletBase memo: memo, credentialsAmount: credentialsAmount, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } else { estimatedTx = await estimateTxForAmount( @@ -1020,6 +1045,7 @@ abstract class ElectrumWalletBase feeRateInt, memo: memo, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index f4d99d807..29a2df48a 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -251,7 +251,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { _syncTimer?.cancel(); try { syncStatus = SyncronizingSyncStatus(); - await subscribeForUpdates(); + try { + await subscribeForUpdates(); + } catch (e) { + print("failed to subcribe for updates: $e"); + } updateFeeRates(); _feeRatesTimer?.cancel(); _feeRatesTimer = @@ -916,6 +920,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } if (!hasMwebInput && !hasMwebOutput) { + tx.isMweb = false; return tx; } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 4c7c49a9e..36d762ea1 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: @@ -336,10 +336,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: @@ -403,14 +403,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" + url: "https://pub.dev" + source: hosted + version: "0.3.1+4" googleapis_auth: dependency: transitive description: name: googleapis_auth - sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da + sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.6.0" graphs: dependency: transitive description: @@ -809,10 +817,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: @@ -1031,10 +1039,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: diff --git a/cw_core/lib/unspent_coin_type.dart b/cw_core/lib/unspent_coin_type.dart new file mode 100644 index 000000000..a042610fc --- /dev/null +++ b/cw_core/lib/unspent_coin_type.dart @@ -0,0 +1 @@ +enum UnspentCoinType { mweb, nonMweb, any } \ No newline at end of file diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index e9e83597a..91232792a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -106,34 +106,33 @@ class CWBitcoin extends Bitcoin { } @override - Object createBitcoinTransactionCredentials(List outputs, - {required TransactionPriority priority, int? feeRate}) { + Object createBitcoinTransactionCredentials( + List outputs, { + required TransactionPriority priority, + int? feeRate, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) { final bitcoinFeeRate = priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null; return BitcoinTransactionCredentials( - outputs - .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, - memo: out.memo)) - .toList(), - priority: priority as BitcoinTransactionPriority, - feeRate: bitcoinFeeRate); + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + memo: out.memo)) + .toList(), + priority: priority as BitcoinTransactionPriority, + feeRate: bitcoinFeeRate, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); } - @override - Object createBitcoinTransactionCredentialsRaw(List outputs, - {TransactionPriority? priority, required int feeRate}) => - BitcoinTransactionCredentials(outputs, - priority: priority != null ? priority as BitcoinTransactionPriority : null, - feeRate: feeRate); - @override @computed List getSubAddresses(Object wallet) { @@ -205,9 +204,20 @@ class CWBitcoin extends Bitcoin { (priority as BitcoinTransactionPriority).labelWithRate(rate, customRate); @override - List getUnspents(Object wallet) { + List getUnspents(Object wallet, + {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.unspentCoins; + return bitcoinWallet.unspentCoins.where((element) { + switch(coinTypeToSpendFrom) { + case UnspentCoinType.mweb: + return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; + case UnspentCoinType.nonMweb: + return element.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.any: + return true; + } + + }).toList(); } Future updateUnspents(Object wallet) async { diff --git a/lib/di.dart b/lib/di.dart index 0008ad8fd..13ffd839e 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -167,6 +167,7 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_i import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; import 'package:cw_core/nano_account.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/transaction_info.dart'; @@ -725,8 +726,8 @@ Future setup({ getIt.get(), getIt.get())); - getIt.registerFactory( - () => SendViewModel( + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) => SendViewModel( getIt.get(), getIt.get(), getIt.get(), @@ -734,12 +735,13 @@ Future setup({ getIt.get(), _transactionDescriptionBox, getIt.get().wallet!.isHardwareWallet ? getIt.get() : null, + coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, ), ); - getIt.registerFactoryParam( - (PaymentRequest? initialPaymentRequest, _) => SendPage( - sendViewModel: getIt.get(), + getIt.registerFactoryParam( + (PaymentRequest? initialPaymentRequest, coinTypeToSpendFrom) => SendPage( + sendViewModel: getIt.get(param1: coinTypeToSpendFrom), authService: getIt.get(), initialPaymentRequest: initialPaymentRequest, )); @@ -1215,14 +1217,21 @@ Future setup({ getIt.registerFactory(() => SupportOtherLinksPage(getIt.get())); - getIt.registerFactory(() { + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) { final wallet = getIt.get().wallet; - return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource); + return UnspentCoinsListViewModel( + wallet: wallet!, + unspentCoinsInfo: _unspentCoinsInfoSource, + coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, + ); }); - getIt.registerFactory(() => - UnspentCoinsListPage(unspentCoinsListViewModel: getIt.get())); + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) => UnspentCoinsListPage( + unspentCoinsListViewModel: + getIt.get(param1: coinTypeToSpendFrom))); getIt.registerFactoryParam( diff --git a/lib/router.dart b/lib/router.dart index 7beace174..3b4c38546 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -120,6 +120,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; @@ -184,7 +185,8 @@ Route createRoute(RouteSettings settings) { final type = settings.arguments as WalletType; final walletGroupsDisplayVM = getIt.get(param1: type); - return CupertinoPageRoute(builder: (_) => WalletGroupsDisplayPage(walletGroupsDisplayVM)); + return CupertinoPageRoute( + builder: (_) => WalletGroupsDisplayPage(walletGroupsDisplayVM)); case Routes.newWallet: final args = settings.arguments as NewWalletArguments; @@ -348,13 +350,17 @@ Route createRoute(RouteSettings settings) { settings: settings, builder: (_) => getIt.get()); case Routes.send: - final initialPaymentRequest = settings.arguments as PaymentRequest?; + final args = settings.arguments as Map?; + final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?; + final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?; return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get( - param1: initialPaymentRequest, - )); + fullscreenDialog: true, + builder: (_) => getIt.get( + param1: initialPaymentRequest, + param2: coinTypeToSpendFrom, + ), + ); case Routes.sendTemplate: return CupertinoPageRoute( @@ -604,7 +610,9 @@ Route createRoute(RouteSettings settings) { fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.unspentCoinsList: - return MaterialPageRoute(builder: (_) => getIt.get()); + final coinTypeToSpendFrom = settings.arguments as UnspentCoinType?; + return MaterialPageRoute( + builder: (_) => getIt.get(param1: coinTypeToSpendFrom)); case Routes.unspentCoinsDetails: final args = settings.arguments as List; @@ -778,7 +786,7 @@ Route createRoute(RouteSettings settings) { case Routes.walletGroupDescription: final walletType = settings.arguments as WalletType; - + return MaterialPageRoute( builder: (_) => WalletGroupDescriptionPage( selectedWalletType: walletType, diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 3b88c685a..708941023 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -17,13 +17,15 @@ import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -837,216 +839,300 @@ class BalanceRowWidget extends StatelessWidget { color: Theme.of(context).extension()!.syncedBackgroundColor, ), child: Container( - margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Stack( - children: [ - if (currency == CryptoCurrency.ltc) - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - padding: EdgeInsets.only(right: 16, top: 16), - child: Column( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, + Container( + margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + child: Stack( + children: [ + if (currency == CryptoCurrency.ltc) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: EdgeInsets.only(right: 16, top: 16), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Color.fromARGB(255, 11, 70, 129), + size: 40, + ), ), - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), - size: 40, + const SizedBox(height: 10), + Text( + 'MWEB', + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ], + ), + if (hasSecondAvailableBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, ), ), - const SizedBox(height: 10), - Text( - 'MWEB', + SizedBox(height: 8), + AutoSizeText( + secondAvailableBalance, style: TextStyle( - fontSize: 15, + fontSize: 20, fontFamily: 'Lato', - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w400, color: Theme.of(context) .extension()! .assetTitleColor, height: 1, ), + maxLines: 1, + textAlign: TextAlign.center, ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAvailableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .textColor, + height: 1, + ), + ), ], ), - ), - ], - ), - if (hasSecondAvailableBalance) - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAvailableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAvailableBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) + ], + ), + ], + ), + ), + Container( + margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + child: Stack( + children: [ + if (hasSecondAdditionalBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), Text( - '${secondAvailableFiatBalance}', + '${secondAdditionalBalanceLabel}', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.textColor, + color: Theme.of(context) + .extension()! + .labelTextColor, height: 1, ), ), - ], - ), - ], - ), - ], - ), - Stack( - children: [ - if (hasSecondAdditionalBalance) - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAdditionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAdditionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${secondAdditionalFiatBalance}', - textAlign: TextAlign.center, + SizedBox(height: 8), + AutoSizeText( + secondAdditionalBalance, style: TextStyle( - fontSize: 12, + fontSize: 20, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.textColor, + color: Theme.of(context) + .extension()! + .assetTitleColor, height: 1, ), + maxLines: 1, + textAlign: TextAlign.center, ), - ], - ), - ], - ), - // TODO: smarter peg in / out buttons - // if (currency == CryptoCurrency.ltc) - // Row( - // mainAxisAlignment: MainAxisAlignment.end, - // children: [ - // Container( - // margin: EdgeInsets.only(top: 24, right: 8), - // child: ElevatedButton( - // style: ElevatedButton.styleFrom( - // backgroundColor: Theme.of(context).highlightColor, - // ), - // onPressed: () { - // final mwebAddress = - // bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); - // if (mwebAddress == null) return; - // final paymentRequest = - // PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); - // Navigator.of(context) - // .pushNamed(Routes.send, arguments: paymentRequest); - // }, - // child: Container( - // color: Colors.transparent, - // margin: EdgeInsets.all(4), - // child: Column( - // mainAxisSize: MainAxisSize.max, - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // Container( - // alignment: Alignment.center, - // decoration: BoxDecoration(shape: BoxShape.circle), - // child: Image.asset( - // 'assets/images/received.png', - // color: Theme.of(context) - // .extension()! - // .balanceAmountColor, - // width: 64, - // height: 32, - // ), - // ), - // SizedBox(height: 4), - // Text( - // S.of(context).litecoin_mweb_pegin, - // style: TextStyle( - // fontSize: 10, - // color: Theme.of(context) - // .extension()! - // .cardTextColor), - // ) - // ], - // ), - // ), - // ), - // ), - // ], - // ), - ], + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAdditionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .textColor, + height: 1, + ), + ), + ], + ), + ], + ), + ], + ), ), + IntrinsicHeight( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Semantics( + label: S.of(context).litecoin_mweb_pegin, + child: OutlinedButton( + onPressed: () { + final mwebAddress = + bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); + PaymentRequest? paymentRequest = null; + if ((mwebAddress?.isNotEmpty ?? false)) { + paymentRequest = + PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Theme.of(context) + .extension()! + .textFieldButtonIconColor + .withAlpha(50), + side: BorderSide(color: Colors.grey.shade400, width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/received.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).litecoin_mweb_pegin, + style: TextStyle( + color: Theme.of(context) + .extension()! + .assetTitleColor, + ), + ), + ], + ), + ), + ), + ), + ), + SizedBox(width: 32), + Expanded( + child: Semantics( + label: S.of(context).litecoin_mweb_pegout, + child: OutlinedButton( + onPressed: () { + final litecoinAddress = + bitcoin!.getAddress(dashboardViewModel.wallet); + PaymentRequest? paymentRequest = null; + if (litecoinAddress.isNotEmpty) { + paymentRequest = PaymentRequest.fromUri( + Uri.parse("litecoin:${litecoinAddress}")); + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.mweb, + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Theme.of(context) + .extension()! + .textFieldButtonIconColor + .withAlpha(50), + side: BorderSide(color: Colors.grey.shade400, width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/upload.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).litecoin_mweb_pegout, + style: TextStyle( + color: Theme.of(context) + .extension()! + .assetTitleColor, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + SizedBox(height: 16), ], ), ), diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 2a14da305..0713fb8c4 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -14,7 +14,6 @@ import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; @@ -373,7 +372,10 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Navigator.of(context).pushNamed(Routes.unspentCoinsList), + onTap: () => Navigator.of(context).pushNamed( + Routes.unspentCoinsList, + arguments: widget.sendViewModel.coinTypeToSpendFrom, + ), child: Container( color: Colors.transparent, child: Row( diff --git a/lib/view_model/link_view_model.dart b/lib/view_model/link_view_model.dart index 99aed486e..27f0c0560 100644 --- a/lib/view_model/link_view_model.dart +++ b/lib/view_model/link_view_model.dart @@ -65,15 +65,16 @@ class LinkViewModel { if (isNanoGptLink) { switch (currentLink?.authority ?? '') { case "exchange": - case "send": return PaymentRequest.fromUri(currentLink); + case "send": + return {"paymentRequest": PaymentRequest.fromUri(currentLink)}; case "buy": return true; } } if (_isValidPaymentUri) { - return PaymentRequest.fromUri(currentLink); + return {"paymentRequest": PaymentRequest.fromUri(currentLink)}; } return null; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 79f473eb3..0ad8ba376 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -20,6 +20,7 @@ import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:hive/hive.dart'; @@ -67,8 +68,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.balanceViewModel, this.contactListViewModel, this.transactionDescriptionBox, - this.ledgerViewModel, - ) : state = InitialExecutionState(), + this.ledgerViewModel, { + this.coinTypeToSpendFrom = UnspentCoinType.any, + }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || @@ -97,6 +99,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ObservableList outputs; + final UnspentCoinType coinTypeToSpendFrom; + @action void addOutput() { outputs @@ -217,7 +221,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor PendingTransaction? pendingTransaction; @computed - String get balance => wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance; + String get balance { + if (coinTypeToSpendFrom == UnspentCoinType.mweb) { + return balanceViewModel.balances.values.first.secondAvailableBalance; + } else if (coinTypeToSpendFrom == UnspentCoinType.nonMweb) { + return balanceViewModel.balances.values.first.availableBalance; + } + return wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance; + } @computed bool get isFiatDisabled => balanceViewModel.isFiatDisabled; @@ -494,8 +505,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: - return bitcoin!.createBitcoinTransactionCredentials(outputs, - priority: priority!, feeRate: customBitcoinFeeRate); + return bitcoin!.createBitcoinTransactionCredentials( + outputs, + priority: priority!, + feeRate: customBitcoinFeeRate, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); case WalletType.monero: return monero! diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 72dcdb27b..f16b8390f 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_base.dart'; @@ -16,9 +17,11 @@ part 'unspent_coins_list_view_model.g.dart'; class UnspentCoinsListViewModel = UnspentCoinsListViewModelBase with _$UnspentCoinsListViewModel; abstract class UnspentCoinsListViewModelBase with Store { - UnspentCoinsListViewModelBase( - {required this.wallet, required Box unspentCoinsInfo}) - : _unspentCoinsInfo = unspentCoinsInfo, + UnspentCoinsListViewModelBase({ + required this.wallet, + required Box unspentCoinsInfo, + this.coinTypeToSpendFrom = UnspentCoinType.any, + }) : _unspentCoinsInfo = unspentCoinsInfo, _items = ObservableList() { _updateUnspentCoinsInfo(); _updateUnspents(); @@ -26,6 +29,7 @@ abstract class UnspentCoinsListViewModelBase with Store { WalletBase wallet; final Box _unspentCoinsInfo; + final UnspentCoinType coinTypeToSpendFrom; @observable ObservableList _items; @@ -103,7 +107,7 @@ abstract class UnspentCoinsListViewModelBase with Store { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: - return bitcoin!.getUnspents(wallet); + return bitcoin!.getUnspents(wallet, coinTypeToSpendFrom: coinTypeToSpendFrom); default: return List.empty(); } diff --git a/tool/configure.dart b/tool/configure.dart index eb41c5341..ce079dd29 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -87,6 +87,7 @@ import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_base.dart'; @@ -94,6 +95,7 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -108,7 +110,6 @@ import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; -import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -119,8 +120,6 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; -import 'package:cw_core/get_height_by_date.dart'; -import 'package:cw_core/transaction_info.dart'; import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart'; import 'package:mobx/mobx.dart'; """; @@ -166,8 +165,7 @@ abstract class Bitcoin { int getFeeRate(Object wallet, TransactionPriority priority); Future generateNewAddress(Object wallet, String label); Future updateAddress(Object wallet,String address, String label); - Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}); - Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}); + Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); String getAddress(Object wallet); List getSilentPaymentAddresses(Object wallet); @@ -181,7 +179,7 @@ abstract class Bitcoin { int formatterStringDoubleToBitcoinAmount(String amount); String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate}); - List getUnspents(Object wallet); + List getUnspents(Object wallet, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); Future updateUnspents(Object wallet); WalletService createBitcoinWalletService( Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); From a211eab435b26fd28e555ce6095d74a6d3934fa8 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Mon, 14 Oct 2024 17:31:27 -0400 Subject: [PATCH 121/142] Change exchange to swap (#1731) * Change exchange strings to swap * Remove bad value --- res/values/strings_ar.arb | 10 +++++----- res/values/strings_bg.arb | 10 +++++----- res/values/strings_cs.arb | 10 +++++----- res/values/strings_de.arb | 10 +++++----- res/values/strings_en.arb | 10 +++++----- res/values/strings_es.arb | 8 ++++---- res/values/strings_fr.arb | 8 ++++---- res/values/strings_ha.arb | 10 +++++----- res/values/strings_hi.arb | 10 +++++----- res/values/strings_hr.arb | 10 +++++----- res/values/strings_hy.arb | 10 +++++----- res/values/strings_id.arb | 10 +++++----- res/values/strings_it.arb | 10 +++++----- res/values/strings_ja.arb | 10 +++++----- res/values/strings_ko.arb | 8 ++++---- res/values/strings_my.arb | 10 +++++----- res/values/strings_nl.arb | 10 +++++----- res/values/strings_pl.arb | 8 ++++---- res/values/strings_pt.arb | 8 ++++---- res/values/strings_ru.arb | 10 +++++----- res/values/strings_th.arb | 6 +++--- res/values/strings_tl.arb | 8 ++++---- res/values/strings_tr.arb | 8 ++++---- res/values/strings_uk.arb | 10 +++++----- res/values/strings_ur.arb | 8 ++++---- res/values/strings_vi.arb | 10 +++++----- res/values/strings_yo.arb | 10 +++++----- res/values/strings_zh.arb | 10 +++++----- 28 files changed, 130 insertions(+), 130 deletions(-) diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 3797334e9..81fe3cc2c 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -114,7 +114,7 @@ "change_currency": "تغيير العملة", "change_current_node": "هل أنت متأكد من تغيير العقدة الحالية إلى ${node}؟", "change_current_node_title": "تغيير العقدة الحالية", - "change_exchange_provider": "تغيير مزود الصرف", + "change_exchange_provider": "تغيير مزود المبادلة", "change_language": "تغيير اللغة", "change_language_to": "هل تريد تغيير اللغة إلى ${language}؟", "change_password": "تغيير كلمة المرور", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan تاريخ", "event": "ﺙﺪﺣ", "events": "ﺙﺍﺪﺣﻷﺍ", - "exchange": "تبادل", - "exchange_incorrect_current_wallet_for_xmr": "إذا كنت ترغب في استبدال XMR من رصيد Cake Wallet Monero ، فيرجى التبديل إلى محفظة Monero أولاً.", + "exchange": "تبديل", + "exchange_incorrect_current_wallet_for_xmr": "إذا كنت ترغب في تبديل XMR من رصيد محفظة الكعكة ، فيرجى التبديل إلى محفظة Monero أولاً.", "exchange_new_template": "قالب جديد", "exchange_provider_unsupported": "${providerName} لم يعد مدعومًا!", "exchange_result_confirm": "بالضغط على تأكيد ، سترسل ${fetchingLabel} ${from} من محفظتك المسماة ${walletName} إلى العنوان الموضح أدناه. أو يمكنك الإرسال من محفظتك الخارجية إلى العنوان أدناه / QR.\n\nيرجى الضغط على تأكيد للمتابعة أو الرجوع لتغيير المبالغ.", @@ -309,7 +309,7 @@ "fill_code": "يرجى ملء رمز التحقق المرسل إلى بريدك الإلكتروني", "filter_by": "تصفية حسب", "first_wallet_text": "محفظة رائعة ل Monero, Bitcoin, Ethereum, Litecoin و Haven", - "fixed_pair_not_supported": "هذا الزوج الثابت غير مدعوم في التبادلات المحددة", + "fixed_pair_not_supported": "لا يتم دعم هذا الزوج الثابت مع خدمات المبادلة المحددة", "fixed_rate": "السعر الثابت", "fixed_rate_alert": "ستتمكن من إدخال مبلغ الاستلام عند تشغيل وضع السعر الثابت. هل تريد التبديل إلى وضع السعر الثابت؟", "forgot_password": "هل نسيت كلمة السر", @@ -439,7 +439,7 @@ "node_test": "تجربة", "nodes": "العقد", "nodes_list_reset_to_default_message": "هل أنت متأكد أنك تريد إعادة تعيين الإعدادات إلى الافتراضي؟", - "none_of_selected_providers_can_exchange": "لا يمكن لأي من مقدمي الخدمة المختارين إجراء هذا التبادل", + "none_of_selected_providers_can_exchange": "لا يمكن لأي من مقدمي الخدمات المختارين إجراء هذا المبادلة", "noNFTYet": "ﻥﻵﺍ ﻰﺘﺣ NFTs ﺪﺟﻮﻳ ﻻ", "normal": "طبيعي", "note_optional": "ملاحظة (اختياري)", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 1d674530b..50db1610a 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -114,7 +114,7 @@ "change_currency": "Смени валута", "change_current_node": "Сигурни ли сте, че искате да промените сегашния node на ${node}?", "change_current_node_title": "Промени сегашния node", - "change_exchange_provider": "Промяна на Exchange Provider", + "change_exchange_provider": "Промяна на доставчика на суап", "change_language": "Смяна на езика", "change_language_to": "Смяна на езика на ${language}?", "change_password": "Смяна на парола", @@ -281,8 +281,8 @@ "etherscan_history": "История на Etherscan", "event": "Събитие", "events": "събития", - "exchange": "Exchange", - "exchange_incorrect_current_wallet_for_xmr": "Ако искате да обмените XMR от своя Cake Wallet Monero баланс, първо изберете своя Monero портфейл.", + "exchange": "Разметка", + "exchange_incorrect_current_wallet_for_xmr": "Ако искате да смените XMR от вашия баланс на портфейла на тортата Monero, моля, преминете първо към вашия портфейл Monero.", "exchange_new_template": "Нов шаблон", "exchange_provider_unsupported": "${providerName} вече не се поддържа!", "exchange_result_confirm": "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown below. Or you can send from your external wallet to the below address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.", @@ -309,7 +309,7 @@ "fill_code": "Моля, въведето кода за потвърждаване, изпратен на Вашия имейл", "filter_by": "Филтрирай по", "first_wallet_text": "Невероятен портфейл за Monero, Bitcoin, Ethereum, Litecoin и Haven", - "fixed_pair_not_supported": "Този fixed pair не се поддържа от избраната борса", + "fixed_pair_not_supported": "Тази фиксирана двойка не се поддържа с избраните услуги за суап", "fixed_rate": "Постоянен обменен курс", "fixed_rate_alert": "Ще можете да въведете сумата за получаване, когато е избранен постоянен обменен курс. Искате ли да изберете постоянен обменен курс?", "forgot_password": "Забравена парола", @@ -439,7 +439,7 @@ "node_test": "Тест", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Сигурни ли сте, че искате да възстановите фабричните настройки?", - "none_of_selected_providers_can_exchange": "Нито един от избраните provider-ъри не може да направи този превод", + "none_of_selected_providers_can_exchange": "Нито един от избраните доставчици не може да направи този размяна", "noNFTYet": "Все още няма NFT", "normal": "нормално", "note_optional": "Бележка (не е задължително)", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index b97c1e9f8..ddc91340b 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -114,7 +114,7 @@ "change_currency": "Změnit měnu", "change_current_node": "Opravdu chcete změnit současný uzel na ${node}?", "change_current_node_title": "Změnit současný uzel", - "change_exchange_provider": "Změnit směnárnu", + "change_exchange_provider": "Změnit poskytovatele swapu", "change_language": "Změnit jazyk", "change_language_to": "Změnit jazyk na ${language}?", "change_password": "Změnit heslo", @@ -281,8 +281,8 @@ "etherscan_history": "Historie Etherscanu", "event": "událost", "events": "Události", - "exchange": "Směnit", - "exchange_incorrect_current_wallet_for_xmr": "Pokud chcete směnit XMR z Monero částky v Cake Wallet, prosím přepněte se nejprve do své Monero peněženky.", + "exchange": "Swap", + "exchange_incorrect_current_wallet_for_xmr": "Pokud chcete vyměnit XMR z vaší dortové peněženky Monero Balance, nejprve přepněte na peněženku Monero.", "exchange_new_template": "Nová šablona", "exchange_provider_unsupported": "${providerName} již není podporováno!", "exchange_result_confirm": "Po stisknutí Potvrdit odešlete ${fetchingLabel} ${from} ze své peněženky s názvem ${walletName} na adresu uvedenou níže. Nebo můžete prostředky poslat ze své externí peněženky na níže uvedenou adresu/QR kód.\n\nProsím stiskněte Potvrdit pro pokračování, nebo se vraťte zpět pro změnu částky.", @@ -309,7 +309,7 @@ "fill_code": "Prosím vyplňte ověřovací kód zaslaný na Váš e-mail", "filter_by": "Filtrovat podle", "first_wallet_text": "Úžasná peněženka pro Monero, Bitcoin, Ethereum, Litecoin a Haven", - "fixed_pair_not_supported": "Tento pár s pevným kurzem není ve zvolené směnárně podporován", + "fixed_pair_not_supported": "Tento pevný pár není podporován vybranými službami swapu", "fixed_rate": "Pevný kurz", "fixed_rate_alert": "Když je zvolený pevný kurz, můžete zadat konkrétní částku, kterou chcete dostat. Chcete se přepnout do režimu s pevným kurzem?", "forgot_password": "Zapomenuté heslo", @@ -439,7 +439,7 @@ "node_test": "Otestovat", "nodes": "Uzly", "nodes_list_reset_to_default_message": "Opravdu chcete zrušit nastavení a vrátit výchozí hodnotu?", - "none_of_selected_providers_can_exchange": "Žádný ze zvolených poskytovatelů nemůže provést tuto směnu", + "none_of_selected_providers_can_exchange": "Žádný z vybraných poskytovatelů nemůže tuto swap provést", "noNFTYet": "Zatím žádné NFT", "normal": "Normální", "note_optional": "Poznámka (nepovinné)", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 80c33a3b7..80433ae0e 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -114,7 +114,7 @@ "change_currency": "Währung ändern", "change_current_node": "Möchten Sie den aktuellen Knoten wirklich zu ${node}? ändern?", "change_current_node_title": "Aktuellen Knoten ändern", - "change_exchange_provider": "Exchange-Anbieter ändern", + "change_exchange_provider": "Swap -Anbieter ändern", "change_language": "Sprache ändern", "change_language_to": "Sprache zu ${language} ändern?", "change_password": "Passwort ändern", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan-Geschichte", "event": "Ereignis", "events": "Veranstaltungen", - "exchange": "Umwechseln", - "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR von Ihrem Cake Wallet Monero-Guthaben umtauschen möchten, wechseln Sie bitte zuerst zu Ihrer Monero-Wallet.", + "exchange": "Tauschen", + "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR aus Ihrer Cake -Wallet Monero -Balance austauschen möchten, wechseln Sie zuerst zu Ihrer Monero -Brieftasche.", "exchange_new_template": "Neue Vorlage", "exchange_provider_unsupported": "${providerName} wird nicht mehr unterstützt!", "exchange_result_confirm": "Durch Drücken von \"Bestätigen\" wird ${fetchingLabel} ${from} von Ihrer Wallet namens ${walletName} an die unten angegebene Adresse gesendet. Alternativ können Sie von einer externen Wallet an die unten angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu ändern.", @@ -309,7 +309,7 @@ "fill_code": "Geben Sie den Bestätigungscode ein, den Sie per E-Mail erhalten haben", "filter_by": "Filtern nach", "first_wallet_text": "Eine großartige Wallet für Monero, Bitcoin, Ethereum, Litecoin, und Haven", - "fixed_pair_not_supported": "Dieses feste Paar wird von den ausgewählten Vermittlungsstellen nicht unterstützt", + "fixed_pair_not_supported": "Dieses feste Paar wird nicht von den ausgewählten Swap -Diensten unterstützt", "fixed_rate": "Feste Rate", "fixed_rate_alert": "Sie können den Empfangsbetrag eingeben, wenn der Festratenmodus aktiviert ist. Möchten Sie in den Festratenmodus wechseln?", "forgot_password": "Passwort vergessen", @@ -439,7 +439,7 @@ "node_test": "Test", "nodes": "Knoten", "nodes_list_reset_to_default_message": "Möchten Sie wirklich die Standardeinstellungen wiederherstellen?", - "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Austausch vornehmen", + "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Tausch machen", "noNFTYet": "Noch keine NFTs", "normal": "Normal", "note_optional": "Bemerkung (optional)", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index b1547be88..d6a0ee9af 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -114,7 +114,7 @@ "change_currency": "Change Currency", "change_current_node": "Are you sure to change current node to ${node}?", "change_current_node_title": "Change current node", - "change_exchange_provider": "Change Exchange Provider", + "change_exchange_provider": "Change Swap Provider", "change_language": "Change language", "change_language_to": "Change language to ${language}?", "change_password": "Change password", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan history", "event": "Event", "events": "Events", - "exchange": "Exchange", - "exchange_incorrect_current_wallet_for_xmr": "If you want to exchange XMR from your Cake Wallet Monero balance, please switch to your Monero wallet first.", + "exchange": "Swap", + "exchange_incorrect_current_wallet_for_xmr": "If you want to swap XMR from your Cake Wallet Monero balance, please switch to your Monero wallet first.", "exchange_new_template": "New template", "exchange_provider_unsupported": "${providerName} is no longer supported!", "exchange_result_confirm": "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown below. Or you can send from your external wallet to the below address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.", @@ -309,7 +309,7 @@ "fill_code": "Please fill in the verification code provided to your email", "filter_by": "Filter by", "first_wallet_text": "Awesome wallet for Monero, Bitcoin, Ethereum, Litecoin, and Haven", - "fixed_pair_not_supported": "This fixed pair is not supported with the selected exchanges", + "fixed_pair_not_supported": "This fixed pair is not supported with the selected swap services", "fixed_rate": "Fixed rate", "fixed_rate_alert": "You will be able to enter receive amount when fixed rate mode is checked. Do you want to switch to fixed rate mode?", "forgot_password": "Forgot Password", @@ -441,7 +441,7 @@ "node_test": "Test", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Are you sure that you want to reset settings to default?", - "none_of_selected_providers_can_exchange": "None of the selected providers can make this exchange", + "none_of_selected_providers_can_exchange": "None of the selected providers can make this swap", "noNFTYet": "No NFTs yet", "normal": "Normal", "note_optional": "Note (optional)", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c4e6bb21f..a8ad7549a 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -281,8 +281,8 @@ "etherscan_history": "historia de etherscan", "event": "Evento", "events": "Eventos", - "exchange": "Intercambiar", - "exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR de su saldo de Cake Wallet Monero, primero cambie a su billetera Monero.", + "exchange": "Intercambio", + "exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR desde su billetera de pastel Monero Balance, primero cambie a su billetera Monero.", "exchange_new_template": "Nueva plantilla", "exchange_provider_unsupported": "¡${providerName} ya no es compatible!", "exchange_result_confirm": "Al presionar confirmar, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra a continuación. O puede enviar desde su billetera externa a la siguiente dirección / código QR anterior.\n\nPresione confirmar para continuar o regrese para cambiar los montos.", @@ -309,7 +309,7 @@ "fill_code": "Por favor complete el código de verificación proporcionado a su correo electrónico", "filter_by": "Filtrado por", "first_wallet_text": "Impresionante billetera para Monero, Bitcoin, Ethereum, Litecoin, y Haven", - "fixed_pair_not_supported": "Este par fijo no es compatible con los intercambios seleccionados", + "fixed_pair_not_supported": "Este par fijo no es compatible con los servicios de intercambio seleccionados", "fixed_rate": "Tipo de interés fijo", "fixed_rate_alert": "Podrá ingresar la cantidad recibida cuando el modo de tarifa fija esté marcado. ¿Quieres cambiar al modo de tarifa fija?", "forgot_password": "Olvidé mi contraseña", @@ -439,7 +439,7 @@ "node_test": "Prueba", "nodes": "Nodos", "nodes_list_reset_to_default_message": "¿Está seguro de que desea restablecer la configuración predeterminada?", - "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede realizar este intercambio", + "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede hacer este intercambio", "noNFTYet": "Aún no hay NFT", "normal": "Normal", "note_optional": "Nota (opcional)", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 8508dda99..be5b48dd8 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -114,7 +114,7 @@ "change_currency": "Changer de Devise", "change_current_node": "Êtes vous certain de vouloir changer le nœud actuel pour ${node} ?", "change_current_node_title": "Changer le nœud actuel", - "change_exchange_provider": "Changer de Plateforme d'Échange", + "change_exchange_provider": "Changer le fournisseur d'échange", "change_language": "Changer de langue", "change_language_to": "Changer la langue vers ${language} ?", "change_password": "Changer le mot de passe", @@ -282,7 +282,7 @@ "event": "Événement", "events": "Événements", "exchange": "Échanger", - "exchange_incorrect_current_wallet_for_xmr": "Si vous souhaitez échanger des XMR du solde Monero de votre Cake Wallet, merci de sélectionner votre portefeuille (wallet) Monero au préalable.", + "exchange_incorrect_current_wallet_for_xmr": "Si vous souhaitez échanger XMR à partir de votre balance monero portefeuille de gâteau, veuillez d'abord passer à votre portefeuille Monero.", "exchange_new_template": "Nouveau modèle d'échange", "exchange_provider_unsupported": "${providerName} n'est plus pris en charge !", "exchange_result_confirm": "En pressant confirmer, vous enverrez ${fetchingLabel} ${from} depuis votre portefeuille (wallet) nommé ${walletName} vers l'adresse ci-dessous. Vous pouvez aussi envoyer depuis votre portefeuille externe vers l'adresse/QR code ci-dessous.\n\nMerci d'appuyer sur confirmer pour continuer ou retournez en arrière pour modifier les montants.", @@ -309,7 +309,7 @@ "fill_code": "Veuillez remplir le code de vérification fourni sur votre e-mail", "filter_by": "Filtrer par", "first_wallet_text": "Super portefeuille (wallet) pour Monero, Bitcoin, Ethereum, Litecoin et Haven", - "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés", + "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les services d'échange sélectionnés", "fixed_rate": "Taux fixe", "fixed_rate_alert": "Vous aurez la possibilité de rentrer le montant reçu lorsque le mode taux fixe est sélectionné. Souhaitez vous basculer en mode taux fixe ?", "forgot_password": "Mot de passe oublié", @@ -439,7 +439,7 @@ "node_test": "Tester", "nodes": "Nœuds", "nodes_list_reset_to_default_message": "Êtes vous certain de vouloir revenir aux réglages par défaut ?", - "none_of_selected_providers_can_exchange": "Aucun des fournisseurs sélectionnés ne peut effectuer cet échange", + "none_of_selected_providers_can_exchange": "Aucun des fournisseurs sélectionnés ne peut faire cet échange", "noNFTYet": "Pas encore de NFT", "normal": "Normal", "note_optional": "Note (optionnelle)", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index b25cc14f5..4deb0df1d 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -114,7 +114,7 @@ "change_currency": "Canja Kuɗi", "change_current_node": "Kuna tabbatar kuna so ku canja node yanzu zuwa ${node}?", "change_current_node_title": "Canja node yanzu", - "change_exchange_provider": "Canza Mai Bayar da Musanya", + "change_exchange_provider": "Canza mai canzawa", "change_language": "canja harshen", "change_language_to": "canja harshen zuwa ${language}?", "change_password": "Canza kalmar shiga", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan tarihin kowane zamani", "event": "Lamarin", "events": "Abubuwan da suka faru", - "exchange": "Exchange", - "exchange_incorrect_current_wallet_for_xmr": "Idan kana son musanya XMR daga ma'aunin Cake Wallet Monero, da fatan za a fara canza wallet ɗin Monero ɗin ku.", + "exchange": "Musya", + "exchange_incorrect_current_wallet_for_xmr": "Idan kana son canza XMR daga walat ɗin Bed Wallet ɗinka, da fatan za a canza zuwa walat ɗinku na Monero.", "exchange_new_template": "Sabon template", "exchange_provider_unsupported": "${providerName}", "exchange_result_confirm": "Ta danna tabbatarwa, zaku aika ${fetchingLabel} ${from} daga walat ɗin ku mai suna ${walletName} zuwa address dake kasa. Ko zaka iya aika daga kwalinku na external zuwa address/QR code dake kasa.\n\nDon Allah shigar da confirm don ci gaba ko dawo ka canja adadinku.", @@ -309,7 +309,7 @@ "fill_code": "Da fatan za a cika lambar tabbatarwa da aka bayar zuwa imel ɗin ku", "filter_by": "Tace ta", "first_wallet_text": "Aikace-aikacen e-wallet ga Monero, Bitcoin, Ethereum, Litecoin, da kuma Haven", - "fixed_pair_not_supported": "Wannan kafaffen guda biyu ba shi da tallafi tare da zaɓaɓɓun musayar", + "fixed_pair_not_supported": "Ba a tallafa wa wannan adireshin da aka zaɓi tare da zaɓin siye ba", "fixed_rate": "Kafaffen ƙima", "fixed_rate_alert": "Za ku iya shigar da adadin karɓa lokacin da aka duba ƙayyadadden zaɓin ƙimar kuɗi. Kuna so ku canza zuwa ƙayyadadden yanayin ƙimar kuɗi?", "forgot_password": "Manta Kalmar wucewa", @@ -439,7 +439,7 @@ "node_test": "Gwaji", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Kuna tabbatar kuna so ku sake saitunan zuwa default?", - "none_of_selected_providers_can_exchange": "Babu ɗaya daga cikin zaɓaɓɓun masu samarwa da zai iya yin wannan musayar", + "none_of_selected_providers_can_exchange": "Babu wani daga cikin masu siye da aka zaɓa na iya yin wannan musan", "noNFTYet": "Babu NFTs tukuna", "normal": "Na al'ada", "note_optional": "Bayani (optional)", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index c00184218..5161250fc 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -114,7 +114,7 @@ "change_currency": "मुद्रा परिवर्तन करें", "change_current_node": "क्या आप वर्तमान नोड को बदलना सुनिश्चित करते हैं ${node}?", "change_current_node_title": "वर्तमान नोड बदलें", - "change_exchange_provider": "एक्सचेंज प्रदाता बदलें", + "change_exchange_provider": "स्वैप प्रदाता बदलें", "change_language": "भाषा बदलो", "change_language_to": "को भाषा बदलें ${language}?", "change_password": "पासवर्ड बदलें", @@ -281,8 +281,8 @@ "etherscan_history": "इथरस्कैन इतिहास", "event": "आयोजन", "events": "आयोजन", - "exchange": "अदला बदली", - "exchange_incorrect_current_wallet_for_xmr": "यदि आप अपने केक वॉलेट मोनेरो बैलेंस से एक्सएमआर का आदान-प्रदान करना चाहते हैं, तो कृपया अपने मोनेरो वॉलेट में जाएं।", + "exchange": "बदलना", + "exchange_incorrect_current_wallet_for_xmr": "यदि आप अपने केक वॉलेट मोनेरो बैलेंस से XMR को स्वैप करना चाहते हैं, तो कृपया पहले अपने मोनेरो वॉलेट पर स्विच करें।", "exchange_new_template": "नया टेम्पलेट", "exchange_provider_unsupported": "${providerName} अब समर्थित नहीं है!", "exchange_result_confirm": "पुष्टि दबाकर, आप भेज रहे होंगे ${fetchingLabel} ${from} अपने बटुए से ${walletName} नीचे दिखाए गए पते पर। या आप अपने बाहरी वॉलेट से नीचे के पते पर भेज सकते हैं / क्यूआर कोड पर भेज सकते हैं।\n\nकृपया जारी रखने या राशि बदलने के लिए वापस जाने के लिए पुष्टि करें दबाएं.", @@ -309,7 +309,7 @@ "fill_code": "कृपया अपने ईमेल पर प्रदान किया गया सत्यापन कोड भरें", "filter_by": "के द्वारा छनित", "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin, और Haven के लिए बहुत बढ़िया बटुआ", - "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", + "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित स्वैप सेवाओं के साथ समर्थित नहीं है", "fixed_rate": "निर्धारित दर", "fixed_rate_alert": "फिक्स्ड रेट मोड की जांच करने पर आप प्राप्त राशि दर्ज कर पाएंगे। क्या आप निश्चित दर मोड पर स्विच करना चाहते हैं?", "forgot_password": "पासवर्ड भूल गए", @@ -439,7 +439,7 @@ "node_test": "परीक्षा", "nodes": "नोड्स", "nodes_list_reset_to_default_message": "क्या आप वाकई सेटिंग को डिफ़ॉल्ट पर रीसेट करना चाहते हैं?", - "none_of_selected_providers_can_exchange": "चयनित प्रदाताओं में से कोई भी इस एक्सचेंज को नहीं बना सकता", + "none_of_selected_providers_can_exchange": "चयनित प्रदाता में से कोई भी यह स्वैप नहीं कर सकता है", "noNFTYet": "अभी तक कोई एनएफटी नहीं", "normal": "सामान्य", "note_optional": "नोट (वैकल्पिक)", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 030d6f49b..8ef92aaf0 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -114,7 +114,7 @@ "change_currency": "Promijenite valutu", "change_current_node": "Jeste li sigurni da želite promijeniti trenutni node na ${node}?", "change_current_node_title": "Promijeni trenutni node", - "change_exchange_provider": "Promjena davatelja usluge razmjene", + "change_exchange_provider": "Promijenite davatelja zamjene", "change_language": "Promijeni jezik", "change_language_to": "Promijeni jezik u ${language}?", "change_password": "Promijeni lozinku", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan povijest", "event": "Događaj", "events": "Događaji", - "exchange": "Razmijeni", - "exchange_incorrect_current_wallet_for_xmr": "Ako želite razmijeniti XMR s vlastitog Monero računa na Cake Wallet novčaniku, molimo prvo se prebacite na svoj Monero novčanik.", + "exchange": "Zamjena", + "exchange_incorrect_current_wallet_for_xmr": "Ako želite zamijeniti XMR iz vašeg novčanika za kolač Monero, prvo se prebacite na svoj novčanik Monero.", "exchange_new_template": "Novi predložak", "exchange_provider_unsupported": "${providerName} više nije podržan!", "exchange_result_confirm": "Pritiskom na potvrdi, poslat ćete ${fetchingLabel} ${from} sa svog novčanika pod nazivom ${walletName} na adresu prikazanu ispod ili iznos možete poslati s vanjskog novčanika na niže navedenu adresu. /QR code.\n\nMolimo potvrdite za nastavak ili se vratite natrag za promjenu iznosa.", @@ -309,7 +309,7 @@ "fill_code": "Molimo vas da ispunite kontrolni kod koji ste dobili na svojoj e-pošti", "filter_by": "Filtrirati po", "first_wallet_text": "Odličan novčanik za Monero, Bitcoin, Ethereum, Litecoin, i Haven", - "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim burzama", + "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim SWAP uslugama", "fixed_rate": "Fiksna stopa", "fixed_rate_alert": "Moći ćete unijeti iznos koji želite primiti nakon što označite način rada fiksne stope. Želite li se prebaciti na način rada fiksne stope?", "forgot_password": "Zaboravljena lozinka", @@ -439,7 +439,7 @@ "node_test": "Provjeri", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Jeste li sigurni da se želite vratiti na početne postavke?", - "none_of_selected_providers_can_exchange": "Niti jedan od odabranih pružatelja usluga ne može izvršiti ovu razmjenu", + "none_of_selected_providers_can_exchange": "Nijedan od odabranih pružatelja usluga ne može napraviti ovu zamjenu", "noNFTYet": "Još nema NFT-ova", "normal": "Normalno", "note_optional": "Poruka (nije obvezno)", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 1869001a7..40ed1e116 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -114,7 +114,7 @@ "change_currency": "Փոխել արժույթը", "change_current_node": "Վստահ եք, որ ցանկանում եք փոխել ընթացիկ հանգույցը ${node}?", "change_current_node_title": "Փոխել ընթացիկ հանգույցը", - "change_exchange_provider": "Փոխել փոխանակման մատակարարին", + "change_exchange_provider": "Փոխեք փոխանակման մատակարարը", "change_language": "Փոխել լեզուն", "change_language_to": "Փոխել լեզուն ${language}?", "change_password": "Փոխել գաղտնաբառը", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan պատմություն", "event": "Իրադարձություն", "events": "Իրադարձություններ", - "exchange": "Փոխել", - "exchange_incorrect_current_wallet_for_xmr": "Եթե դուք ցանկանում եք փոխանակել XMR ձեր Cake Wallet Monero հաշվեհամարից, խնդրում ենք անցնել ձեր Monero հաշվեհամարին", + "exchange": "Փոխանակել", + "exchange_incorrect_current_wallet_for_xmr": "Եթե ​​ցանկանում եք փոխանակել XMR ձեր տորթի դրամապանակից Monero Relandal- ից, խնդրում ենք նախ անցնել ձեր Monero դրամապանակին:", "exchange_new_template": "Նոր տեսակ", "exchange_provider_unsupported": "${providerName} այլևս չի ապահովվում", "exchange_result_confirm": "Սեղմելով հաստատել, դուք կուղարկեք ${fetchingLabel} ${from} ձեր հաշվեհամարից ${walletName} հետևյալ հասցեին: Կամ կարող եք ուղարկել ձեր արտաքին հաշվեհամարից հետևյալ հասցեին/QR կոդին:\n\nԽնդրում ենք սեղմել հաստատել կամ վերադառնալ գումարը փոխելու համար", @@ -309,7 +309,7 @@ "fill_code": "Խնդրում ենք լրացնել հաստատման կոդը ձեր էլեկտրոնային փոստում", "filter_by": "Ֆիլտրել ըստ", "first_wallet_text": "Հիանալի հաշվեհամար Monero, Bitcoin, Ethereum, Litecoin և Haven արժույթների համար", - "fixed_pair_not_supported": "Այս ֆիքսված զույգը չի ապահովվում ընտրված փոխանակման կետերում", + "fixed_pair_not_supported": "Այս ֆիքսված զույգը չի ապահովվում ընտրված փոխանակման ծառայություններ", "fixed_rate": "Ֆիքսված փոխարժեք", "fixed_rate_alert": "Դուք կկարողանաք մուտքագրել ստացվող գումարը, երբ ֆիքսված փոխարժեքի ռեժիմը միացված է: Դուք ցանկանում եք անցնել ֆիքսված փոխարժեքի ռեժիմին?", "forgot_password": "Մոռացել եմ գաղտնաբառը", @@ -431,7 +431,7 @@ "node_test": "Փորձարկում", "nodes": "Հանգույցներ", "nodes_list_reset_to_default_message": "Վերակայվում եք կարգավորումները լռությամբ?", - "none_of_selected_providers_can_exchange": "Ոչ մի ընտրված մատակարար չի կարող այս փոխանակումը կատարել", + "none_of_selected_providers_can_exchange": "Ընտրված մատակարարներից ոչ մեկը չի կարող կատարել այս փոխանակումը", "noNFTYet": "Դեռ ոչ մի NFT", "normal": "Նորմալ", "note_optional": "Նշում (ոչ պարտադիր)", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index b215fa148..5f93082ec 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -114,7 +114,7 @@ "change_currency": "Ganti Mata Uang", "change_current_node": "Apakah Anda yakin ingin mengubah node saat ini menjadi ${node}?", "change_current_node_title": "Ubah node saat ini", - "change_exchange_provider": "Ganti Penyedia Tukar", + "change_exchange_provider": "Ubah penyedia swap", "change_language": "Ganti bahasa", "change_language_to": "Ganti bahasa ke ${language}?", "change_password": "Ubah kata sandi", @@ -281,8 +281,8 @@ "etherscan_history": "Sejarah Etherscan", "event": "Peristiwa", "events": "Acara", - "exchange": "Tukar", - "exchange_incorrect_current_wallet_for_xmr": "Jika Anda ingin menukar XMR dari saldo Monero Cake Wallet Anda, silakan beralih ke dompet Monero Anda terlebih dahulu.", + "exchange": "Menukar", + "exchange_incorrect_current_wallet_for_xmr": "Jika Anda ingin bertukar XMR dari Saldo Monero Dompet Kue Anda, silakan beralih ke Monero Wallet Anda terlebih dahulu.", "exchange_new_template": "Template baru", "exchange_provider_unsupported": "${providerName} tidak lagi didukung!", "exchange_result_confirm": "Dengan menekan tombol konfirmasi, Anda akan mengirimkan ${fetchingLabel} ${from} dari dompet Anda yang disebut ${walletName} ke alamat yang ditampilkan di bawah. Anda juga dapat mengirim dari dompet eksternal Anda ke alamat/QR code di bawah.\n\nSilakan tekan konfirmasi untuk melanjutkan atau kembali untuk mengubah jumlah.", @@ -309,7 +309,7 @@ "fill_code": "Silakan isi kode verifikasi yang diterima di email Anda", "filter_by": "Filter berdasarkan", "first_wallet_text": "Dompet luar biasa untuk Monero, Bitcoin, Ethereum, Litecoin, dan Haven", - "fixed_pair_not_supported": "Pasangan tetap ini tidak didukung dengan bursa yang dipilih", + "fixed_pair_not_supported": "Pasangan tetap ini tidak didukung dengan layanan swap yang dipilih", "fixed_rate": "Rate tetap", "fixed_rate_alert": "Anda akan dapat memasukkan jumlah penerimaan saat mode rate tetap dicentang. Apakah Anda ingin beralih ke mode rate tetap?", "forgot_password": "Lupa Kata Sandi", @@ -439,7 +439,7 @@ "node_test": "Uji", "nodes": "Node", "nodes_list_reset_to_default_message": "Apakah Anda yakin ingin mengatur ulang pengaturan ke default?", - "none_of_selected_providers_can_exchange": "Tidak ada dari penyedia yang dipilih yang dapat melakukan pertukaran ini", + "none_of_selected_providers_can_exchange": "Tak satu pun dari penyedia yang dipilih dapat melakukan pertukaran ini", "noNFTYet": "Belum ada NFT", "normal": "Normal", "note_optional": "Catatan (opsional)", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 14963f58b..08ae928af 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -114,7 +114,7 @@ "change_currency": "Cambia Valuta", "change_current_node": "Sei sicuro di voler cambiare il nodo corrente con ${node}?", "change_current_node_title": "Cambia nodo corrente", - "change_exchange_provider": "Cambia Exchange", + "change_exchange_provider": "Provider di swap di cambiamento", "change_language": "Cambia lingua", "change_language_to": "Cambiare lingua in ${language}?", "change_password": "Cambia password", @@ -282,8 +282,8 @@ "etherscan_history": "Storia Etherscan", "event": "Evento", "events": "Eventi", - "exchange": "Scambia", - "exchange_incorrect_current_wallet_for_xmr": "Se vuoi scambiare XMR dal tuo saldo Cake Wallet Monero, gentilmente passa al tuo portafoglio Monero.", + "exchange": "Scambio", + "exchange_incorrect_current_wallet_for_xmr": "Se vuoi scambiare XMR dal tuo portafoglio di torta Monero Balance, si prega di passare prima al portafoglio Monero.", "exchange_new_template": "Nuovo modello", "exchange_provider_unsupported": "${providerName} non è più supportato!", "exchange_result_confirm": "Cliccando su Conferma, invierai ${fetchingLabel} ${from} dal tuo portafoglio chiamato ${walletName} all'indirizzo mostrato qui in basso. O puoi inviare dal tuo portafoglio esterno all'indirizzo/codice QR mostrato in basso.\n\nGentilmente clicca su Conferma per continuare o torna indietro per cambiare l'ammontare.", @@ -310,7 +310,7 @@ "fill_code": "Compila il codice di verifica fornito alla tua email", "filter_by": "Filtrirati po", "first_wallet_text": "Portafoglio fantastico per Monero, Bitcoin, Ethereum, Litecoin, e Haven", - "fixed_pair_not_supported": "Questa coppia fissa non è supportata con gli scambi selezionati", + "fixed_pair_not_supported": "Questa coppia fissa non è supportata con i servizi di swap selezionati", "fixed_rate": "Tasso fisso", "fixed_rate_alert": "Potrai inserire l'ammontare da ricevere quando il tasso è fisso. Vuoi cambiare alla modalità tasso fisso?", "forgot_password": "Password dimenticata", @@ -440,7 +440,7 @@ "node_test": "Test", "nodes": "Nodi", "nodes_list_reset_to_default_message": "Sei sicuro di voler ripristinare le impostazioni predefinite?", - "none_of_selected_providers_can_exchange": "Nessuno dei fornitori selezionati può effettuare questo scambio", + "none_of_selected_providers_can_exchange": "Nessuno dei provider selezionati può fare questo scambio", "noNFTYet": "Nessun NFT ancora", "normal": "Normale", "note_optional": "Nota (opzionale)", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index af61ab1f9..d70eca31b 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -114,7 +114,7 @@ "change_currency": "通貨を変更する", "change_current_node": "現在のノードを変更してよろしいですか ${node}?", "change_current_node_title": "現在のノードを変更する", - "change_exchange_provider": "Exchangeプロバイダーの変更", + "change_exchange_provider": "スワッププロバイダーを変更します", "change_language": "言語を変えてください", "change_language_to": "言語を変更 ${language}?", "change_password": "パスワードを変更する", @@ -281,8 +281,8 @@ "etherscan_history": "イーサスキャンの歴史", "event": "イベント", "events": "イベント", - "exchange": "交換する", - "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet Moneroの残高からXMRを交換する場合は、最初にMoneroウォレットに切り替えてください。", + "exchange": "スワップ", + "exchange_incorrect_current_wallet_for_xmr": "XMRをケーキウォレットモネロバランスから交換したい場合は、最初にMoneroウォレットに切り替えてください。", "exchange_new_template": "新しいテンプレート", "exchange_provider_unsupported": "${providerName}はサポートされなくなりました!", "exchange_result_confirm": "確認を押すと、送信されます ${fetchingLabel} ${from} と呼ばれるあなたの財布から ${walletName} 下記の住所へ。 または、外部ウォレットから以下のアドレスに送信することもできます/ QRコードに送信できます.\n\n確認を押して続行するか、戻って金額を変更してください.", @@ -309,7 +309,7 @@ "fill_code": "メールアドレスに記載されている確認コードを入力してください", "filter_by": "でフィルタリング", "first_wallet_text": "Monero、Bitcoin、Ethereum、Litecoin、Haven用の素晴らしいウォレット", - "fixed_pair_not_supported": "この固定ペアは、選択したエクスチェンジではサポートされていません", + "fixed_pair_not_supported": "この固定ペアは、選択したスワップサービスではサポートされていません", "fixed_rate": "固定金利", "fixed_rate_alert": "固定金利モードにチェックを入れると、受取額を入力できるようになります。 固定金利モードに切り替えますか?", "forgot_password": "パスワードを忘れた", @@ -440,7 +440,7 @@ "node_test": "テスト", "nodes": "ノード", "nodes_list_reset_to_default_message": "設定をデフォルトにリセットしてもよろしいですか?", - "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこの交換を行うことができません", + "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこのスワップを作成できません", "noNFTYet": "NFTはまだありません", "normal": "普通", "note_optional": "注(オプション)", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index dd19af792..133ca1838 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -114,7 +114,7 @@ "change_currency": "통화 변경", "change_current_node": "현재 노드를 다음으로 변경 하시겠습니까 ${node}?", "change_current_node_title": "현재 노드 변경", - "change_exchange_provider": "교환 공급자 변경", + "change_exchange_provider": "스왑 제공 업체를 변경하십시오", "change_language": "언어 변경", "change_language_to": "언어를로 변경 ${language}?", "change_password": "비밀번호 변경", @@ -282,7 +282,7 @@ "event": "이벤트", "events": "이벤트", "exchange": "교환", - "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet Monero 잔액에서 XMR을 교환하려면 먼저 Monero 지갑으로 전환하십시오.", + "exchange_incorrect_current_wallet_for_xmr": "케이크 지갑 Monero Balance에서 XMR을 교체하려면 먼저 Monero 지갑으로 전환하십시오.", "exchange_new_template": "새 템플릿", "exchange_provider_unsupported": "${providerName}은 더 이상 지원되지 않습니다!", "exchange_result_confirm": "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 아래 주소로. 또는 외부 지갑에서 아래 주소로 보낼 수 있습니다 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오.", @@ -309,7 +309,7 @@ "fill_code": "이메일에 제공된 인증 코드를 입력하세요.", "filter_by": "필터링 기준", "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin 및 Haven을 위한 멋진 지갑", - "fixed_pair_not_supported": "이 고정 쌍은 선택한 교환에서 지원되지 않습니다.", + "fixed_pair_not_supported": "이 고정 쌍은 선택한 스왑 서비스에서 지원되지 않습니다.", "fixed_rate": "고정 비율", "fixed_rate_alert": "고정 금리 모드 체크시 수취 금액 입력이 가능합니다. 고정 속도 모드로 전환 하시겠습니까?", "forgot_password": "비밀번호 찾기", @@ -439,7 +439,7 @@ "node_test": "테스트", "nodes": "노드", "nodes_list_reset_to_default_message": "설정을 기본값으로 재설정 하시겠습니까?", - "none_of_selected_providers_can_exchange": "선택한 공급자 중 누구도 이 교환을 할 수 없습니다.", + "none_of_selected_providers_can_exchange": "선택한 공급자 중 어느 것도이 교환을 할 수 없습니다", "noNFTYet": "아직 NFT가 없습니다", "normal": "정상", "note_optional": "참고 (선택 사항)", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 8ce050339..1727f0d71 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -114,7 +114,7 @@ "change_currency": "ငွေကြေးကိုပြောင်းပါ။", "change_current_node": "လက်ရှိ နှာခေါင်း ကို ${node} သို့ ပြောင်းရန် သေချာပါသလား။", "change_current_node_title": "လက်ရှိ နှာခေါင်း ကိုပြောင်းပါ။", - "change_exchange_provider": "အပြန်အလှန် လဲလှယ်ရေး ထောက်ပံ့ပေးသူကို ပြောင်းလဲပါ", + "change_exchange_provider": "SWAP ပံ့ပိုးသူပြောင်းလဲပါ", "change_language": "ဘာသာစကားပြောင်းပါ။", "change_language_to": "ဘာသာစကားကို ${language} သို့ ပြောင်းမလား။", "change_password": "စကားဝှက်ကိုပြောင်းရန်", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan သမိုင်း", "event": "ပွဲ", "events": "အဲ့ဒါနဲ့", - "exchange": "ချိန်းတယ်။", - "exchange_incorrect_current_wallet_for_xmr": "သင်၏ Cake Wallet Monero လက်ကျန်မှ XMR ကိုလဲလှယ်လိုပါက၊ သင်၏ Monero ပိုက်ဆံအိတ်သို့ ဦးစွာပြောင်းပါ။", + "exchange": "လဲလှယ်", + "exchange_incorrect_current_wallet_for_xmr": "အကယ်. သင်သည် XMR ကိုသင်၏ကိတ်မုန့် Monero Balance မှ Swap ကိုလဲလှယ်လိုပါကသင်၏ Monero Wallet ကိုပထမဆုံးအကြိမ်ပြောင်းပါ။", "exchange_new_template": "ပုံစံအသစ်", "exchange_provider_unsupported": "${providerName} မရှိတော့ပါ!", "exchange_result_confirm": "အတည်ပြုချက်ကို နှိပ်ခြင်းဖြင့်၊ သင်သည် ${fetchingLabel} ${from} ဟုခေါ်သော သင့်ပိုက်ဆံအိတ်မှ ${walletName} ကို အောက်ဖော်ပြပါလိပ်စာသို့ ပေးပို့မည်ဖြစ်ပါသည်။ သို့မဟုတ် သင့်ပြင်ပပိုက်ဆံအိတ်မှ အောက်ပါလိပ်စာ/QR ကုဒ်သို့ ပေးပို့နိုင်ပါသည်။\n\nပမာဏများကို ပြောင်းလဲရန် ဆက်လက်လုပ်ဆောင်ရန် သို့မဟုတ် ပြန်သွားရန် အတည်ပြုချက်ကို နှိပ်ပါ။", @@ -309,7 +309,7 @@ "fill_code": "သင့်အီးမေးလ်သို့ ပေးထားသည့် အတည်ပြုကုဒ်ကို ဖြည့်ပါ။", "filter_by": "အလိုက် စစ်ထုတ်ပါ။", "first_wallet_text": "Monero၊ Bitcoin၊ Ethereum၊ Litecoin နှင့် Haven အတွက် အလွန်ကောင်းမွန်သော ပိုက်ဆံအိတ်", - "fixed_pair_not_supported": "ရွေးချယ်ထားသော ဖလှယ်မှုများဖြင့် ဤပုံသေအတွဲကို ပံ့ပိုးမထားပါ။", + "fixed_pair_not_supported": "ဤပုံသေစုံတွဲသည်ရွေးချယ်ထားသောလဲလှယ်ရေးအစီအစဉ်များဖြင့်မထောက်ပံ့ပါ", "fixed_rate": "ပုံသေနှုန်း", "fixed_rate_alert": "ပုံသေနှုန်းထားမုဒ်ကို စစ်ဆေးသည့်အခါ လက်ခံပမာဏကို ထည့်သွင်းနိုင်မည်ဖြစ်သည်။ ပုံသေနှုန်းမုဒ်သို့ ပြောင်းလိုပါသလား။", "forgot_password": "စကားဝှက်မေ့နေပါသလား", @@ -439,7 +439,7 @@ "node_test": "စမ်း", "nodes": "ဆုံမှတ်များ", "nodes_list_reset_to_default_message": "ဆက်တင်များကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်လိုသည်မှာ သေချာပါသလား။", - "none_of_selected_providers_can_exchange": "ရွေးချယ်ထားသော ဝန်ဆောင်မှုပေးသူများမှ ဤလဲလှယ်မှုကို ပြုလုပ်၍မရပါ။", + "none_of_selected_providers_can_exchange": "ရွေးချယ်ထားသောပံ့ပိုးပေးသူတစ် ဦး တစ်ယောက်မှဤအစီအစဉ်ကိုလုပ်နိုင်သည်", "noNFTYet": "NFTs မရှိသေးပါ။", "normal": "ပုံမှန်", "note_optional": "မှတ်ချက် (ချန်လှပ်ထားနိုင်သည်)", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 6b1dd29c5..3f2df531b 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -114,7 +114,7 @@ "change_currency": "Verander valuta", "change_current_node": "Weet u zeker dat u het huidige knooppunt wilt wijzigen in ${node}?", "change_current_node_title": "Wijzig het huidige knooppunt", - "change_exchange_provider": "Wijzig Exchange Provider", + "change_exchange_provider": "Verander Swap Provider", "change_language": "Verander de taal", "change_language_to": "Verander de taal in ${language}?", "change_password": "Wachtwoord wijzigen", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan-geschiedenis", "event": "Evenement", "events": "Evenementen", - "exchange": "Uitwisseling", - "exchange_incorrect_current_wallet_for_xmr": "Als u XMR wilt omwisselen van uw Cake Wallet Monero-saldo, moet u eerst overschakelen naar uw Monero-portemonnee.", + "exchange": "Ruil", + "exchange_incorrect_current_wallet_for_xmr": "Als je XMR uit je cake -portemonnee Monero -balans wilt ruilen, schakel dan eerst over naar je Monero -portemonnee.", "exchange_new_template": "Nieuwe sjabloon", "exchange_provider_unsupported": "${providerName} wordt niet langer ondersteund!", "exchange_result_confirm": "Door op bevestigen te drukken, wordt u verzonden ${fetchingLabel} ${from} uit je portemonnee genoemd ${walletName} naar het onderstaande adres. Of u kunt vanuit uw externe portemonnee naar het onderstaande adres verzenden / QR-code sturen.\n\nDruk op bevestigen om door te gaan of terug te gaan om de bedragen te wijzigen.", @@ -309,7 +309,7 @@ "fill_code": "Vul de verificatiecode in die u in uw e-mail hebt ontvangen", "filter_by": "Filteren op", "first_wallet_text": "Geweldige portemonnee voor Monero, Bitcoin, Ethereum, Litecoin, en Haven", - "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund bij de geselecteerde exchanges", + "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund met de geselecteerde swap -services", "fixed_rate": "Vast tarief", "fixed_rate_alert": "U kunt het ontvangen bedrag invoeren wanneer de modus voor vaste tarieven is aangevinkt. Wilt u overschakelen naar de vaste-tariefmodus?", "forgot_password": "Wachtwoord vergeten", @@ -439,7 +439,7 @@ "node_test": "Test", "nodes": "Knooppunten", "nodes_list_reset_to_default_message": "Weet u zeker dat u de standaardinstellingen wilt herstellen?", - "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze uitwisseling maken", + "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze swap maken", "noNFTYet": "Nog geen NFT's", "normal": "Normaal", "note_optional": "Opmerking (optioneel)", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 4bf323c1c..91b265144 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -114,7 +114,7 @@ "change_currency": "Zmień walutę", "change_current_node": "Czy na pewno chcesz wybrać ten węzeł? ${node}?", "change_current_node_title": "Zmień bieżący węzeł", - "change_exchange_provider": "Zmień dostawcę wymiany", + "change_exchange_provider": "Zmień dostawca zamiany", "change_language": "Zmień język", "change_language_to": "Zmień język na ${language}?", "change_password": "Zmień hasło", @@ -281,8 +281,8 @@ "etherscan_history": "Historia Etherscanu", "event": "Wydarzenie", "events": "Wydarzenia", - "exchange": "Wymień", - "exchange_incorrect_current_wallet_for_xmr": "Jeśli chcesz wymienić XMR z salda Cake Wallet Monero, najpierw przełącz się na portfel Monero.", + "exchange": "Zamieniać", + "exchange_incorrect_current_wallet_for_xmr": "Jeśli chcesz zamienić XMR z salda Monero Portfer, najpierw przejdź na portfel Monero.", "exchange_new_template": "Nowy szablon wymiany", "exchange_provider_unsupported": "${providerName} nie jest już obsługiwany!", "exchange_result_confirm": "Naciskając Potwierdź, wyślesz ${fetchingLabel} ${from} z twojego portfela ${walletName} na adres podany poniżej. Lub możesz wysłać z zewnętrznego portfela na poniższy adres / kod QR.\n\nNaciśnij Potwierdź, aby kontynuować lub wróć, aby zmienić kwoty.", @@ -309,7 +309,7 @@ "fill_code": "Proszę wpisać kod weryfikacyjny który otrzymałeś w wiadomości e-mail", "filter_by": "Filtruj według", "first_wallet_text": "Świetny portfel na Monero, Bitcoin, Ethereum, Litecoin, i Haven", - "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana na wybranych giełdach", + "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana z wybranymi usługami swap", "fixed_rate": "Stała stawka", "fixed_rate_alert": "Będziesz mógł wprowadzić kwotę do otrzymania, gdy wybrany bedzie tryb stałego przeliczenia. Czy chcesz przejść do trybu stałej stawki?", "forgot_password": "Zapomniałem hasła", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 603dd75b7..524dbcace 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -114,7 +114,7 @@ "change_currency": "Alterar moeda", "change_current_node": "Você realmente deseja alterar o nó atual para ${node}?", "change_current_node_title": "Mudar o nó atual", - "change_exchange_provider": "Alterar o provedor de troca", + "change_exchange_provider": "Provedor de troca de alteração", "change_language": "Mudar idioma", "change_language_to": "Alterar idioma para ${language}?", "change_password": "Mudar senha", @@ -282,7 +282,7 @@ "event": "Evento", "events": "Eventos", "exchange": "Trocar", - "exchange_incorrect_current_wallet_for_xmr": "Se você deseja trocar o XMR de seu saldo da Carteira Monero Cake, troque primeiro para sua carteira Monero.", + "exchange_incorrect_current_wallet_for_xmr": "Se você deseja trocar o XMR do balanço da carteira de bolo, mude para a sua carteira Monero primeiro.", "exchange_new_template": "Novo modelo", "exchange_provider_unsupported": "${providerName} não é mais suportado!", "exchange_result_confirm": "Ao confirmar, você enviará ${fetchingLabel} ${from} da sua carteira ${walletName} para o endereço mostrado abaixo. Ou você pode enviar de sua carteira externa para o endereço abaixo/código QR acima.\n\nPressione Confirmar para continuar ou volte para alterar os valores.", @@ -309,7 +309,7 @@ "fill_code": "Por favor, preencha o código de verificação fornecido ao seu e-mail", "filter_by": "Filtrar por", "first_wallet_text": "Carteira incrível para Monero, Bitcoin, Ethereum, Litecoin, e Haven", - "fixed_pair_not_supported": "Este par fixo não é compatível com as exchanges selecionadas", + "fixed_pair_not_supported": "Este par fixo não é suportado com os serviços de troca selecionados", "fixed_rate": "Taxa fixa", "fixed_rate_alert": "Você poderá inserir a quantia recebida quando o modo de taxa fixa estiver marcado. Quer mudar para o modo de taxa fixa?", "forgot_password": "Esqueci a senha", @@ -440,7 +440,7 @@ "node_test": "Teste", "nodes": "Nós", "nodes_list_reset_to_default_message": "Você realmente deseja redefinir as configurações para o padrão?", - "none_of_selected_providers_can_exchange": "Nenhum dos provedores selecionados pode fazer esta troca", + "none_of_selected_providers_can_exchange": "Nenhum dos fornecedores selecionados pode fazer essa troca", "noNFTYet": "Ainda não há NFT", "normal": "Normal", "note_optional": "Nota (opcional)", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 43d912ffc..1a8c2447f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -114,7 +114,7 @@ "change_currency": "Изменить валюту", "change_current_node": "Вы уверены, что хотите изменить текущую ноду на ${node}?", "change_current_node_title": "Изменить текущую ноду", - "change_exchange_provider": "Изменить провайдера обмена", + "change_exchange_provider": "Изменить поставщика свопа", "change_language": "Изменить язык", "change_language_to": "Изменить язык на ${language}?", "change_password": "Изменить пароль", @@ -281,8 +281,8 @@ "etherscan_history": "История Эфириума", "event": "Событие", "events": "События", - "exchange": "Обмен", - "exchange_incorrect_current_wallet_for_xmr": "Если вы хотите обменять XMR со своего баланса Monero в Cake Wallet, сначала переключитесь на свой кошелек Monero.", + "exchange": "Менять", + "exchange_incorrect_current_wallet_for_xmr": "Если вы хотите поменять XMR с баланса с кошельком для торта Monero, сначала переключитесь на свой кошелек Monero.", "exchange_new_template": "Новый шаблон", "exchange_provider_unsupported": "${providerName} больше не поддерживается!", "exchange_result_confirm": "Нажимая подтвердить, вы отправите ${fetchingLabel} ${from} с вашего кошелька ${walletName} на адрес указанный ниже. Или вы можете отправить со своего внешнего кошелька на нижеуказанный адрес/QR-код.\n\nПожалуйста, нажмите подтвердить для продолжения, или вернитесь назад для изменения суммы.", @@ -309,7 +309,7 @@ "fill_code": "Пожалуйста, введите код подтверждения, отправленный на вашу электронную почту", "filter_by": "Фильтровать по", "first_wallet_text": "В самом удобном кошельке для Monero, Bitcoin, Ethereum, Litecoin, и Haven", - "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными биржами.", + "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными службами свопа", "fixed_rate": "Фиксированная ставка", "fixed_rate_alert": "Вы сможете ввести сумму получения тогда, когда будет установлен режим фиксированной ставки. Вы хотите перейти в режим фиксированной ставки?", "forgot_password": "Забыли пароль", @@ -439,7 +439,7 @@ "node_test": "Тест", "nodes": "Ноды", "nodes_list_reset_to_default_message": "Вы уверены, что хотите сбросить настройки до значений по умолчанию?", - "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может совершить этот обмен", + "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может сделать это обмен", "noNFTYet": "NFT пока нет", "normal": "Нормальный", "note_optional": "Примечание (необязательно)", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index c02c7cc33..213f74530 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -282,7 +282,7 @@ "event": "เหตุการณ์", "events": "กิจกรรม", "exchange": "แลกเปลี่ยน", - "exchange_incorrect_current_wallet_for_xmr": "หากคุณต้องการแลกเปลี่ยน XMR จากยอดคงเหลือ Monero ใน Cake Wallet ของคุณ กรุณาเปลี่ยนเป็นกระเป๋า Monero ก่อน", + "exchange_incorrect_current_wallet_for_xmr": "หากคุณต้องการสลับ XMR จาก Cake Wallet Monero Balance โปรดเปลี่ยนไปใช้กระเป๋าเงิน Monero ก่อน", "exchange_new_template": "เทมเพลทใหม่", "exchange_provider_unsupported": "${providerName} ไม่ได้รับการสนับสนุนอีกต่อไป!", "exchange_result_confirm": "โดยกดปุ่มยืนยัน, คุณจะส่ง ${fetchingLabel} ${from} จากกระเป๋าของคุณที่เรียกว่า ${walletName} ไปยังที่อยู่ที่แสดงข้างล่าง หรือคุณสามารถส่งจากกระเป๋าภายนอกไปยังที่อยู่/รหัส QR ด้านล่าง\n\nโปรดกดปุ่มยืนยันเพื่อดำเนินการต่อหรือกลับไปเปลี่ยนจำนวน", @@ -309,7 +309,7 @@ "fill_code": "โปรดกรอกรหัสยืนยันที่ส่งไปยังอีเมลของคุณ", "filter_by": "กรองตาม", "first_wallet_text": "กระเป๋าสตางค์ที่สวยงามสำหรับ Monero, Bitcoin, Ethereum, Litecoin และ Haven", - "fixed_pair_not_supported": "คู่ความสัมพันธ์ที่ถูกกำหนดไว้นี้ไม่สนับสนุนกับหุ้นที่เลือก", + "fixed_pair_not_supported": "คู่คงที่นี้ไม่ได้รับการสนับสนุนด้วยบริการแลกเปลี่ยนที่เลือก", "fixed_rate": "อัตราคงที่", "fixed_rate_alert": "คุณจะสามารถป้อนจำนวนที่ได้รับเมื่อเลือกโหมดอัตราคงที่ คุณต้องการสลับไปที่โหมดอัตราคงที่?", "forgot_password": "ลืมรหัสผ่าน", @@ -439,7 +439,7 @@ "node_test": "ทดสอบ", "nodes": "โหนด", "nodes_list_reset_to_default_message": "คุณแน่ใจหรือว่าต้องการรีเซ็ตการตั้งค่าเป็นค่าเริ่มต้น?", - "none_of_selected_providers_can_exchange": "ไม่มีผู้ให้บริการที่เลือกที่สามารถแลกเปลี่ยนนี้ได้", + "none_of_selected_providers_can_exchange": "ผู้ให้บริการที่เลือกไม่สามารถทำการแลกเปลี่ยนนี้ได้", "noNFTYet": "ยังไม่มี NFT", "normal": "ปกติ", "note_optional": "บันทึก (ไม่จำเป็น)", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 99d22bb1e..0ca8ee665 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -114,7 +114,7 @@ "change_currency": "Baguhin ang pera", "change_current_node": "Sigurado ka bang baguhin ang kasalukuyang node sa ${node}?", "change_current_node_title": "Baguhin ang kasalukuyang node", - "change_exchange_provider": "Baguhin ang exchange provider", + "change_exchange_provider": "Baguhin ang Swap Provider", "change_language": "Baguhin ang wika", "change_language_to": "Baguhin ang wika sa ${language}?", "change_password": "Baguhin ang password", @@ -282,7 +282,7 @@ "event": "Kaganapan", "events": "Mga kaganapan", "exchange": "Palitan", - "exchange_incorrect_current_wallet_for_xmr": "Kung gusto mong palitan ang XMR mula sa iyong balanse ng Monero ng Cake Wallet, mangyaring lumipat muna sa iyong Monero wallet.", + "exchange_incorrect_current_wallet_for_xmr": "Kung nais mong magpalit ng XMR mula sa iyong balanse ng Wallet Monero, mangyaring lumipat sa iyong Monero Wallet muna.", "exchange_new_template": "Bagong template", "exchange_provider_unsupported": "Ang ${providerName} ay hindi na suportado!", "exchange_result_confirm": "Sa pamamagitan ng pagpindot sa kumpirmahin, ikaw ay magpapadala ${fetchingLabel} ${from} mula sa inyong wallet na tinatawag ${walletName} sa wallet na ipinapakita sa ibaba. O pwede kang magpadala sa inyong external wallet sa ibabang address/QR code.\n\nPara magpatuloy, mangyaring pindutin upang kumpirmahin o bumalik para baguhin ang halaga.", @@ -309,7 +309,7 @@ "fill_code": "Mangyaring ilagay ang verfification code na ibinigay sa iyong email", "filter_by": "Filter ni", "first_wallet_text": "Kahanga-hangang wallet para sa Monero, Bitcoin, Litecoin, Ethereum, at Haven", - "fixed_pair_not_supported": "Ang nakapirming pares na ito ay hindi suportado sa mga napiling palitan", + "fixed_pair_not_supported": "Ang nakapirming pares na ito ay hindi suportado sa mga napiling serbisyo ng pagpapalit", "fixed_rate": "Fixed rate", "fixed_rate_alert": "Makakapagpasok ka ng halaga ng pagtanggap kapag nasuri ang fixed rate mode. Gusto mo bang lumipat sa fixed rate mode?", "forgot_password": "Nakalimutan ang Password", @@ -439,7 +439,7 @@ "node_test": "Test", "nodes": "Mga node", "nodes_list_reset_to_default_message": "Sigurado ka bang gusto mo bang i-reset ang mga settings sa default?", - "none_of_selected_providers_can_exchange": "Wala sa mga napiling provider ang makakagawa ng palitan na ito", + "none_of_selected_providers_can_exchange": "Wala sa mga napiling tagapagbigay ng serbisyo ang maaaring gumawa ng pagpapalit na ito", "noNFTYet": "Wala pang NFT", "normal": "Normal", "note_optional": "Tala (opsyonal)", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 86239cd77..b23f64d60 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -114,7 +114,7 @@ "change_currency": "Para Birimini Değiştir", "change_current_node": "Şimdiki düğümü ${node} düğümüne değiştirmek istediğinizden emin misin?", "change_current_node_title": "Şimdiki düğümü değiştir", - "change_exchange_provider": "Takas sağlayıcısını değiştir", + "change_exchange_provider": "Takas Sağlayıcısı Değiştir", "change_language": "Dili değiştir", "change_language_to": "Dili şuna değiştir: ${language}?", "change_password": "Parolayı değiştir", @@ -282,7 +282,7 @@ "event": "Etkinlik", "events": "Olaylar", "exchange": "Takas", - "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet'daki Monero bakiyenizi kullanarak takas yapmak istiyorsan, lütfen önce Monero cüzdanına geç.", + "exchange_incorrect_current_wallet_for_xmr": "XMR'yi kek cüzdanı Monero bakiyenizden değiştirmek istiyorsanız, lütfen önce Monero cüzdanınıza geçin.", "exchange_new_template": "Yeni şablon", "exchange_provider_unsupported": "${providerName} artık desteklenmiyor!", "exchange_result_confirm": "Onaylaya basarak, ${fetchingLabel} ${from} miktarında ${walletName} olarak adlandırılan cüzdanından aşağıda gösterilen adrese gönderilecek. Veya harici cüzdanından aşağıdaki adrese / QR koduna gönderebilirsin.\n\nLütfen devam etmek için onayla'ya bas veya tutarı değiştirmek için geri dön.", @@ -309,7 +309,7 @@ "fill_code": "Lütfen e-postanıza gelen doğrulama kodunu girin", "filter_by": "Şuna göre filtrele", "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin ve Haven için harika cüzdan", - "fixed_pair_not_supported": "Bu sabit paritesi seçilen borsalarda desteklenmemekte", + "fixed_pair_not_supported": "Bu sabit çift seçilen takas hizmetleri ile desteklenmez", "fixed_rate": "Sabit oran", "fixed_rate_alert": "Sabit oran modunu işaretlersen alım tutarını girebilirsin. Sabit oran moduna geçmek ister misin?", "forgot_password": "Parolamı unuttum", @@ -439,7 +439,7 @@ "node_test": "Test Et", "nodes": "Düğümler", "nodes_list_reset_to_default_message": "Ayarları varsayılana sıfırlamak istediğinizden emin misin?", - "none_of_selected_providers_can_exchange": "Seçilen sağlayıcılardan hiçbiri bu takası yapamaz", + "none_of_selected_providers_can_exchange": "Seçilen sağlayıcıların hiçbiri bu takas yapamaz", "noNFTYet": "Henüz NFT yok", "normal": "Normal", "note_optional": "Not (isteğe bağlı)", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 2d049e12e..79dc0543f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -114,7 +114,7 @@ "change_currency": "Змінити валюту", "change_current_node": "Ви впевнені, що хочете змінити поточний вузол на ${node}?", "change_current_node_title": "Змінити поточний вузол", - "change_exchange_provider": "Змінити провайдера обміну", + "change_exchange_provider": "Змінити постачальник свопів", "change_language": "Змінити мову", "change_language_to": "Змінити мову на ${language}?", "change_password": "Змінити пароль", @@ -281,8 +281,8 @@ "etherscan_history": "Історія Etherscan", "event": "Подія", "events": "Події", - "exchange": "Обмін", - "exchange_incorrect_current_wallet_for_xmr": "Якщо ви хочете обміняти XMR із вашого балансу Cake Wallet Monero, спочатку перейдіть на свій гаманець Monero.", + "exchange": "Обміняти", + "exchange_incorrect_current_wallet_for_xmr": "Якщо ви хочете поміняти XMR зі свого балансу для тортів Monero Balance, спочатку перейдіть на свій гаманець Monero.", "exchange_new_template": "Новий шаблон", "exchange_provider_unsupported": "${providerName} більше не підтримується!", "exchange_result_confirm": "Натиснувши підтвердити, ви відправите ${fetchingLabel} ${from} з вашого гаманця ${walletName} на адресу вказану нижче. Або ви можете відправити зі свого зовнішнього гаманця на нижчевказану адресу/QR-код.\n\nБудь ласка, натисніть підтвердити для продовження або поверніться назад щоб змінити суму.", @@ -309,7 +309,7 @@ "fill_code": "Будь ласка, введіть код підтвердження, надісланий на вашу електронну адресу", "filter_by": "Фільтрувати по", "first_wallet_text": "В самому зручному гаманці для Monero, Bitcoin, Ethereum, Litecoin, та Haven", - "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними біржами", + "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними послугами Swap", "fixed_rate": "Фіксована ставка", "fixed_rate_alert": "Ви зможете ввести суму отримання тоді, коли буде встановлений режим фіксованої ставки. Ви хочете перейти в режим фіксованої ставки?", "forgot_password": "Забули пароль", @@ -439,7 +439,7 @@ "node_test": "Тест", "nodes": "Вузли", "nodes_list_reset_to_default_message": "Ви впевнені, що хочете скинути до налаштувань за замовченням?", - "none_of_selected_providers_can_exchange": "Жоден із вибраних провайдерів не може здійснити цей обмін", + "none_of_selected_providers_can_exchange": "Жоден із вибраних постачальників не може зробити цей своп", "noNFTYet": "NFT ще немає", "normal": "нормальний", "note_optional": "Примітка (необов’язково)", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index f080c23a5..0a136d140 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -114,7 +114,7 @@ "change_currency": "کرنسی تبدیل کریں", "change_current_node": "کیا آپ یقینی طور پر موجودہ نوڈ کو ${node} میں تبدیل کرنا چاہتے ہیں؟", "change_current_node_title": "موجودہ نوڈ کو تبدیل کریں۔", - "change_exchange_provider": "ایکسچینج فراہم کنندہ کو تبدیل کریں۔", + "change_exchange_provider": "تبادلہ فراہم کرنے والے کو تبدیل کریں", "change_language": "زبان تبدیل کریں", "change_language_to": "زبان کو ${language} میں تبدیل کریں؟", "change_password": "پاس ورڈ تبدیل کریں", @@ -282,7 +282,7 @@ "event": "ﺐﯾﺮﻘﺗ", "events": "ﺕﺎﺒﯾﺮﻘﺗ", "exchange": "تبادلہ", - "exchange_incorrect_current_wallet_for_xmr": "اگر آپ اپنے Cake والیٹ Monero بیلنس سے XMR کا تبادلہ کرنا چاہتے ہیں، تو براہ کرم پہلے اپنے Monero والیٹ پر جائیں۔", + "exchange_incorrect_current_wallet_for_xmr": "اگر آپ اپنے کیک پرس مونیرو بیلنس سے XMR تبدیل کرنا چاہتے ہیں تو ، براہ کرم پہلے اپنے مونیرو پرس میں جائیں۔", "exchange_new_template": "نیا سانچہ", "exchange_provider_unsupported": "${providerName} اب تعاون نہیں کیا جاتا ہے!", "exchange_result_confirm": "تصدیق کو دبانے سے، آپ اپنے بٹوے سے ${fetchingLabel} ${from} بھیجیں گے جسے ${walletName} کہتے ہیں نیچے دکھائے گئے پتے پر۔ یا آپ اپنے بیرونی والیٹ سے نیچے دیئے گئے پتے/QR کوڈ پر بھیج سکتے ہیں۔\\n\\nجاری رکھنے کے لیے براہ کرم تصدیق کو دبائیں یا رقم تبدیل کرنے کے لیے واپس جائیں۔", @@ -309,7 +309,7 @@ "fill_code": "براہ کرم اپنے ای میل پر فراہم کردہ تصدیقی کوڈ کو پُر کریں۔", "filter_by": "کی طرف سے فلٹر", "first_wallet_text": "Monero، Bitcoin، Ethereum، Litecoin، اور Haven کے لیے زبردست پرس", - "fixed_pair_not_supported": "یہ مقررہ جوڑا منتخب کردہ تبادلے کے ساتھ تعاون یافتہ نہیں ہے۔", + "fixed_pair_not_supported": "یہ فکسڈ جوڑی منتخب شدہ تبادلہ خدمات کے ساتھ تعاون یافتہ نہیں ہے", "fixed_rate": "مقررہ شرح", "fixed_rate_alert": "فکسڈ ریٹ موڈ چیک ہونے پر آپ وصولی رقم درج کر سکیں گے۔ کیا آپ فکسڈ ریٹ موڈ پر سوئچ کرنا چاہتے ہیں؟", "forgot_password": "پاسورڈ بھول گے", @@ -439,7 +439,7 @@ "node_test": "پرکھ", "nodes": "نوڈس", "nodes_list_reset_to_default_message": "کیا آپ واقعی ترتیبات کو ڈیفالٹ پر دوبارہ ترتیب دینا چاہتے ہیں؟", - "none_of_selected_providers_can_exchange": "منتخب فراہم کنندگان میں سے کوئی بھی یہ تبادلہ نہیں کر سکتا", + "none_of_selected_providers_can_exchange": "منتخب کردہ کوئی بھی فراہم کنندہ یہ تبادلہ نہیں کرسکتا", "noNFTYet": "۔ﮟﯿﮨ ﮟﯿﮩﻧ NFTs ﯽﺋﻮﮐ ﮏﺗ ﯽﮭﺑﺍ", "normal": "نارمل", "note_optional": "نوٹ (اختیاری)", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 63db7ce88..8d28d48a2 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -114,7 +114,7 @@ "change_currency": "Thay đổi Tiền tệ", "change_current_node": "Bạn có chắc chắn muốn thay đổi nút hiện tại sang ${node} không?", "change_current_node_title": "Thay đổi nút hiện tại", - "change_exchange_provider": "Thay đổi Nhà cung cấp Trao đổi", + "change_exchange_provider": "Thay đổi nhà cung cấp hoán đổi", "change_language": "Thay đổi ngôn ngữ", "change_language_to": "Thay đổi ngôn ngữ sang ${language}?", "change_password": "Thay đổi mật khẩu", @@ -282,8 +282,8 @@ "etherscan_history": "Lịch sử Etherscan", "event": "Sự kiện", "events": "Các sự kiện", - "exchange": "Trao đổi", - "exchange_incorrect_current_wallet_for_xmr": "Nếu bạn muốn trao đổi XMR từ số dư Monero của Ví Cake, vui lòng chuyển sang ví Monero của bạn trước.", + "exchange": "Tráo đổi", + "exchange_incorrect_current_wallet_for_xmr": "Nếu bạn muốn trao đổi XMR từ CAPE CAME MONERO BALANCE, vui lòng chuyển sang ví Monero của bạn trước.", "exchange_new_template": "Mẫu mới", "exchange_provider_unsupported": "${providerName} không còn được hỗ trợ nữa!", "exchange_result_confirm": "Bằng cách nhấn xác nhận, bạn sẽ gửi ${fetchingLabel} ${from} từ ví có tên ${walletName} của mình đến địa chỉ dưới đây. Hoặc bạn có thể gửi từ ví bên ngoài của mình đến địa chỉ/mã QR bên dưới.\n\nVui lòng nhấn xác nhận để tiếp tục hoặc quay lại để thay đổi số tiền.", @@ -310,7 +310,7 @@ "fill_code": "Vui lòng điền mã xác minh được gửi đến email của bạn", "filter_by": "Lọc theo", "first_wallet_text": "Ví tuyệt vời cho Monero, Bitcoin, Ethereum, Litecoin, và Haven", - "fixed_pair_not_supported": "Cặp tỷ giá cố định này không được hỗ trợ với các sàn giao dịch đã chọn", + "fixed_pair_not_supported": "Cặp cố định này không được hỗ trợ với các dịch vụ hoán đổi đã chọn", "fixed_rate": "Tỷ giá cố định", "fixed_rate_alert": "Bạn sẽ có thể nhập số lượng nhận được khi chế độ tỷ giá cố định được chọn. Bạn có muốn chuyển sang chế độ tỷ giá cố định không?", "forgot_password": "Quên mật khẩu", @@ -432,7 +432,7 @@ "node_test": "Kiểm tra", "nodes": "Các nút", "nodes_list_reset_to_default_message": "Bạn có chắc chắn muốn đặt lại cài đặt về mặc định không?", - "none_of_selected_providers_can_exchange": "Không có nhà cung cấp nào đã chọn có thể thực hiện giao dịch này", + "none_of_selected_providers_can_exchange": "Không có nhà cung cấp nào được chọn có thể thực hiện hoán đổi này", "noNFTYet": "Chưa có NFT", "normal": "Bình thường", "note_optional": "Ghi chú (tùy chọn)", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 16a887a39..14270120c 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -114,7 +114,7 @@ "change_currency": "Pààrọ̀ irú owó", "change_current_node": "Ṣé ó dá yín lójú pé ẹ fẹ́ pààrọ̀ apẹka lọ́wọ́ sí ${node}?", "change_current_node_title": "Pààrọ̀ apẹka lọwọ́", - "change_exchange_provider": "Pààrọ̀ Ilé Ìfowóṣòwò", + "change_exchange_provider": "Yipada olupese Swap", "change_language": "Pààrọ̀ èdè", "change_language_to": "Pààrọ̀ èdè sí ${language}?", "change_password": "Pààrọ̀ ọ̀rọ̀ aṣínà", @@ -282,8 +282,8 @@ "etherscan_history": "Etherscan itan", "event": "Iṣẹlẹ", "events": "Awọn iṣẹlẹ", - "exchange": "Pàṣípààrọ̀", - "exchange_incorrect_current_wallet_for_xmr": "T'ẹ́ bá fẹ́ pàṣípààrọ̀ XMR láti ìyókù owó Cake Wallet yín, ẹ jọ̀wọ́ kọ́kọ́ sún àpamọ́wọ́ Monero mọ́.", + "exchange": "Eepo", + "exchange_incorrect_current_wallet_for_xmr": "Ti o ba fẹ lati yi XMR lati dọgba oyinbo oyinbo kekere rẹ ti a fi omi ṣan rẹ, jọwọ yipada si apamọwọ Monrou akọkọ.", "exchange_new_template": "Àwòṣe títun", "exchange_provider_unsupported": "${providerName} ko ni atilẹyin mọ!", "exchange_result_confirm": "T'ẹ́ bá tẹ̀ jẹ́rìí, ẹ máa fi ${fetchingLabel} ${from} ránṣẹ́ láti àpamọ́wọ́ yín t'á pe ${walletName} sí àdírẹ́sì t'ó ṣàfihàn òun lísàlẹ̀. Tàbí ẹ lè fi àpamọ́wọ́ mìíràn yín ránṣẹ́ sí àdírẹ́sì / àmì ìlujá lísàlẹ̀.\n\nẸ jọ̀wọ́ tẹ̀ jẹ́rìí́ tẹ̀síwájú tàbí padà sọ́dọ̀ pààrọ̀ iye náà.", @@ -310,7 +310,7 @@ "fill_code": "Ẹ jọ̀wọ́ tẹ̀ ọ̀rọ̀ ìjẹ́rìísí t'á ti ránṣẹ́ sí ímeèlì yín.", "filter_by": "Ṣẹ́ láti", "first_wallet_text": "Àpamọ́wọ́ t'á fi Monero, Bitcoin, Ethereum, Litecoin, àti Haven pamọ́ wà pa", - "fixed_pair_not_supported": "A kì í ṣe k'á fi àwọn ilé pàṣípààrọ̀ yìí ṣe pàṣípààrọ̀ irú owó méji yìí", + "fixed_pair_not_supported": "Bata ti o wa titi ko ṣe atilẹyin pẹlu awọn iṣẹ SWAP ti o yan", "fixed_rate": "Iye t'á ṣẹ́ owó sí ò ní pààrọ̀", "fixed_rate_alert": "Ẹ lè tẹ̀ iye owó tó ń bọ̀ tí iye t'a ṣẹ́ owó sí bá is checked. Ṣé ẹ fẹ́ sún ipò ti iye t'á ṣẹ́ owó sí ò ní pààrọ̀ mọ́?", "forgot_password": "Ẹ ti gbàgbé ọ̀rọ̀ aṣínà", @@ -440,7 +440,7 @@ "node_test": "Dánwò", "nodes": "Àwọn apẹka", "nodes_list_reset_to_default_message": "Ṣé ó dá yín lójú pé ẹ fẹ́ yí àwọn ààtò padà?", - "none_of_selected_providers_can_exchange": "Àwọn ilé pàṣípààrọ̀ yíyàn kò lè ṣe pàṣípààrọ̀ yìí", + "none_of_selected_providers_can_exchange": "Ko si ọkan ninu awọn olupese ti a yan le ṣe ina yii", "noNFTYet": "Ko si awọn NFT sibẹsibẹ", "normal": "Deede", "note_optional": "Àkọsílẹ̀ (ìyàn nìyí)", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 9398e2408..65047b4fe 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -114,7 +114,7 @@ "change_currency": "更改币种", "change_current_node": "您确定将当前节点更改为 ${node}?", "change_current_node_title": "更改当前节点", - "change_exchange_provider": "更改交易所", + "change_exchange_provider": "更改交换提供商", "change_language": "修改语言", "change_language_to": "修改语言为 ${language}?", "change_password": "更改密码", @@ -281,8 +281,8 @@ "etherscan_history": "以太扫描历史", "event": "事件", "events": "活动", - "exchange": "兑换", - "exchange_incorrect_current_wallet_for_xmr": "如果要从Cake Wallet Monero余额中兑换XMR,请先切换到Monero钱包。", + "exchange": "交换", + "exchange_incorrect_current_wallet_for_xmr": "如果您想从蛋糕钱包Monero余额中交换XMR,请先切换到Monero Wallet。", "exchange_new_template": "新模板", "exchange_provider_unsupported": "${providerName}不再支持!", "exchange_result_confirm": "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到下面显示的地址。 或者您可以从外部钱包发送到以下地址/ QR码。\n\n请按确认继续或返回以更改金额", @@ -309,7 +309,7 @@ "fill_code": "请填写提供给您邮箱的验证码", "filter_by": "过滤", "first_wallet_text": "适用于门罗币、比特币、以太坊、莱特币和避风港的超棒钱包", - "fixed_pair_not_supported": "所选交易所不支持此固定货币对", + "fixed_pair_not_supported": "所选的交换服务不支持这对固定对", "fixed_rate": "固定汇率", "fixed_rate_alert": "选中固定汇率模式后,您将可以输入接收金额。 您要切换到固定汇率模式吗?", "forgot_password": "忘记密码", @@ -439,7 +439,7 @@ "node_test": "测试", "nodes": "节点", "nodes_list_reset_to_default_message": "您确定要将设置重设为默认值吗?", - "none_of_selected_providers_can_exchange": "选定的供应商都不能进行此交换", + "none_of_selected_providers_can_exchange": "选定的提供商都无法进行此交换", "noNFTYet": "还没有 NFT", "normal": "普通的", "note_optional": "注释(可选)", From c3019b91cd7c73c3ed7f96da4ddfe68158cf0500 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Mon, 14 Oct 2024 19:10:48 -0400 Subject: [PATCH 122/142] Harden the Android manifest (#1741) * Test with Android manifest modifications * Commit correct changes * Remove permission that was already present --- android/app/src/main/AndroidManifestBase.xml | 37 +++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index b03c8a925..1d0e2c93c 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -1,35 +1,30 @@ - - - - - + + - - + - - - + + + - - - + + + + - + + + + - + + Date: Wed, 16 Oct 2024 02:58:41 +0100 Subject: [PATCH 123/142] fix: Issue with nownodes on older wallets (#1750) --- lib/entities/default_settings_migration.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 1c46cd605..2b8c49631 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -254,6 +254,7 @@ Future defaultSettingsMigration( case 41: _deselectQuantex(sharedPreferences); await _addSethNode(nodes, sharedPreferences); + await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); break; default: break; @@ -1317,3 +1318,16 @@ Future removeMoneroWorld( await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); } } + +Future updateTronNodesWithNowNodes({ + required SharedPreferences sharedPreferences, + required Box nodes, +}) async { + final tronNowNodesUri = 'trx.nownodes.io'; + + if (nodes.values.any((node) => node.uriRaw == tronNowNodesUri)) return; + + await nodes.add(Node(uri: tronNowNodesUri, type: WalletType.tron)); + + await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); +} From cb75f08b35617452ed4a6013172494104c6bfabc Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 16 Oct 2024 16:28:24 +0300 Subject: [PATCH 124/142] fix P2wpkhAddress btc ltc validation (#1749) * fix P2wpkhAddress btc ltc validation * Update address_validator.dart --- lib/core/address_validator.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 0aabfa032..21726fab8 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -36,9 +36,9 @@ class AddressValidator extends TextValidator { '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; case CryptoCurrency.btc: pattern = - '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{25,39}}').pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: - pattern = '^${P2wpkhAddress.regex.pattern}\$|^${MwebAddress.regex.pattern}\$'; + pattern = '^${RegExp(r'ltc1q[ac-hj-np-z02-9]{25,39}').pattern}\$|^${MwebAddress.regex.pattern}\$'; case CryptoCurrency.nano: pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.banano: From 605d164998c0f964267861edfff1972c88e15ab1 Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 16 Oct 2024 20:25:22 +0300 Subject: [PATCH 125/142] Cw 772 restore from qr generates different wallet (#1742) * fix derivation info for QR restoring * allow all available seed languages for Monero * set default derivation info for an empty wallet * fix electrum case --- .../restore/restore_from_qr_vm.dart | 12 +++++- lib/view_model/wallet_creation_vm.dart | 41 +++++++++++-------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 042a048d9..cbdad85b8 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -37,7 +37,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store spendKey = '', wif = '', address = '', - super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true); + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: true); @observable int height; @@ -112,7 +113,14 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store ); case WalletType.bitcoin: case WalletType.litecoin: - final derivationInfo = (await getDerivationInfoFromQRCredentials(restoreWallet)).first; + + final derivationInfoList = await getDerivationInfoFromQRCredentials(restoreWallet); + DerivationInfo derivationInfo; + if (derivationInfoList.isEmpty) { + derivationInfo = getDefaultCreateDerivation()!; + } else { + derivationInfo = derivationInfoList.first; + } return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index f31f879a1..90b4c5c3a 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -194,24 +194,31 @@ abstract class WalletCreationVMBase with Store { final walletType = restoreWallet.type; var appStore = getIt.get(); var node = appStore.settingsStore.getCurrentNode(walletType); + + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: - switch (walletType) { - case WalletType.bitcoin: - case WalletType.litecoin: - return bitcoin!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - passphrase: restoreWallet.passphrase, - ); - case WalletType.nano: - return nanoUtil!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - default: - break; - } - return list; + final derivationList = await bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + + + if (derivationList.first.transactionsCount == 0 && derivationList.length > 1) return []; + + return derivationList; + + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; } WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); From 564f8e2c70c3e377db985097680c377a034e72a9 Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 16 Oct 2024 19:27:04 +0200 Subject: [PATCH 126/142] fix backup on debug builds (#1728) open transactionDecscriptionBox if it's not open --- lib/core/backup_service.dart | 38 ++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index ba90a0449..d65530eb5 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/entities/get_encryption_key.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cw_core/root_dir.dart'; @@ -105,7 +106,15 @@ class BackupService { if (entity.path == archivePath || entity.path == tmpDir.path) { return; } - + final filename = entity.absolute; + for (var ignore in ignoreFiles) { + final filename = entity.absolute.path; + if (filename.endsWith(ignore) && !filename.contains("wallets/")) { + print("ignoring backup file: $filename"); + return; + } + } + print("restoring: $filename"); if (entity.statSync().type == FileSystemEntityType.directory) { zipEncoder.addDirectory(Directory(entity.path)); } else { @@ -148,14 +157,29 @@ class BackupService { await _importPreferencesDump(); } + // checked with .endsWith - so this should be the last part of the filename + static const ignoreFiles = [ + "flutter_assets/kernel_blob.bin", + "flutter_assets/vm_snapshot_data", + "flutter_assets/isolate_snapshot_data", + ".lock", + ]; + Future _importBackupV2(Uint8List data, String password) async { final appDir = await getAppDir(); final decryptedData = await _decryptV2(data, password); final zip = ZipDecoder().decodeBytes(decryptedData); + outer: for (var file in zip.files) { final filename = file.name; - + for (var ignore in ignoreFiles) { + if (filename.endsWith(ignore) && !filename.contains("wallets/")) { + print("ignoring backup file: $filename"); + continue outer; + } + } + print("restoring: $filename"); if (file.isFile) { final content = file.content as List; File('${appDir.path}/' + filename) @@ -206,6 +230,16 @@ class BackupService { json.decode(transactionDescriptionFile.readAsStringSync()) as Map; final descriptionsMap = jsonData.map((key, value) => MapEntry(key, TransactionDescription.fromJson(value as Map))); + + if (!_transactionDescriptionBox.isOpen) { + final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorageShared, forKey: TransactionDescription.boxKey); + final transactionDescriptionBox = await CakeHive.openBox( + TransactionDescription.boxName, + encryptionKey: transactionDescriptionsBoxKey, + ); + await transactionDescriptionBox.putAll(descriptionsMap); + return; + } await _transactionDescriptionBox.putAll(descriptionsMap); } From cd5a1277a7759e11d3f5d0714abb3b8df8e463db Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 17 Oct 2024 02:17:29 +0100 Subject: [PATCH 127/142] Disable sendAll on native EVM Currencies (#1753) * fix: Issue with nownodes on older wallets * fix: Disable sendAll on native evm currencies --- lib/view_model/send/send_view_model.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 0ad8ba376..334394a18 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -123,7 +123,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed bool get isBatchSending => outputs.length > 1; - bool get shouldDisplaySendALL => walletType != WalletType.solana; + bool get shouldDisplaySendALL { + if (walletType == WalletType.solana) return false; + + if (walletType == WalletType.ethereum && selectedCryptoCurrency == CryptoCurrency.eth) + return false; + + if (walletType == WalletType.polygon && selectedCryptoCurrency == CryptoCurrency.matic) + return false; + + return true; + } @computed String get pendingTransactionFiatAmount { From 6c9de67599fb0612ee95a87ab644e8b83ffb858e Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 17 Oct 2024 14:52:55 +0200 Subject: [PATCH 128/142] Fix fiat rate on receive screen. (#1754) * Fix fiat rate on receive screen. * Code cleanup * Update Receive QR on currency change --- .../wallet_address_list_view_model.dart | 231 ++++++++++-------- 1 file changed, 133 insertions(+), 98 deletions(-) diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 0260dc6b4..3e399266a 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,14 +1,15 @@ -import 'dart:math'; +import 'dart:developer' as dev; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -24,16 +25,14 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_i import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; -import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_monero/api/wallet.dart'; -import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; part 'wallet_address_list_view_model.g.dart'; -class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel; +class WalletAddressListViewModel = WalletAddressListViewModelBase + with _$WalletAddressListViewModel; abstract class PaymentURI { PaymentURI({required this.amount, required this.address}); @@ -43,12 +42,11 @@ abstract class PaymentURI { } class MoneroURI extends PaymentURI { - MoneroURI({required String amount, required String address}) - : super(amount: amount, address: address); + MoneroURI({required super.amount, required super.address}); @override String toString() { - var base = 'monero:' + address; + var base = 'monero:$address'; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -59,12 +57,11 @@ class MoneroURI extends PaymentURI { } class HavenURI extends PaymentURI { - HavenURI({required String amount, required String address}) - : super(amount: amount, address: address); + HavenURI({required super.amount, required super.address}); @override String toString() { - var base = 'haven:' + address; + var base = 'haven:$address'; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -75,12 +72,11 @@ class HavenURI extends PaymentURI { } class BitcoinURI extends PaymentURI { - BitcoinURI({required String amount, required String address}) - : super(amount: amount, address: address); + BitcoinURI({required super.amount, required super.address}); @override String toString() { - var base = 'bitcoin:' + address; + var base = 'bitcoin:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -91,12 +87,11 @@ class BitcoinURI extends PaymentURI { } class LitecoinURI extends PaymentURI { - LitecoinURI({required String amount, required String address}) - : super(amount: amount, address: address); + LitecoinURI({required super.amount, required super.address}); @override String toString() { - var base = 'litecoin:' + address; + var base = 'litecoin:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -107,12 +102,11 @@ class LitecoinURI extends PaymentURI { } class EthereumURI extends PaymentURI { - EthereumURI({required String amount, required String address}) - : super(amount: amount, address: address); + EthereumURI({required super.amount, required super.address}); @override String toString() { - var base = 'ethereum:' + address; + var base = 'ethereum:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -123,8 +117,7 @@ class EthereumURI extends PaymentURI { } class BitcoinCashURI extends PaymentURI { - BitcoinCashURI({required String amount, required String address}) - : super(amount: amount, address: address); + BitcoinCashURI({required super.amount, required super.address}); @override String toString() { @@ -139,12 +132,11 @@ class BitcoinCashURI extends PaymentURI { } class NanoURI extends PaymentURI { - NanoURI({required String amount, required String address}) - : super(amount: amount, address: address); + NanoURI({required super.amount, required super.address}); @override String toString() { - var base = 'nano:' + address; + var base = 'nano:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -154,12 +146,11 @@ class NanoURI extends PaymentURI { } class PolygonURI extends PaymentURI { - PolygonURI({required String amount, required String address}) - : super(amount: amount, address: address); + PolygonURI({required super.amount, required super.address}); @override String toString() { - var base = 'polygon:' + address; + var base = 'polygon:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -170,12 +161,12 @@ class PolygonURI extends PaymentURI { } class SolanaURI extends PaymentURI { - SolanaURI({required String amount, required String address}) - : super(amount: amount, address: address); + SolanaURI({required super.amount, required super.address}); @override String toString() { - var base = 'solana:' + address; + var base = 'solana:$address'; + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -185,12 +176,12 @@ class SolanaURI extends PaymentURI { } class TronURI extends PaymentURI { - TronURI({required String amount, required String address}) - : super(amount: amount, address: address); + TronURI({required super.amount, required super.address}); @override String toString() { - var base = 'tron:' + address; + var base = 'tron:$address'; + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -200,12 +191,11 @@ class TronURI extends PaymentURI { } class WowneroURI extends PaymentURI { - WowneroURI({required String amount, required String address}) - : super(amount: amount, address: address); + WowneroURI({required super.amount, required super.address}); @override String toString() { - var base = 'wownero:' + address; + var base = 'wownero:$address'; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -215,7 +205,8 @@ class WowneroURI extends PaymentURI { } } -abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { +abstract class WalletAddressListViewModelBase + extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, required this.yatStore, @@ -223,9 +214,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), - hasAccounts = appStore.wallet!.type == WalletType.monero || - appStore.wallet!.type == WalletType.wownero || - appStore.wallet!.type == WalletType.haven, + hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] + .contains(appStore.wallet!.type), amount = '', _settingsStore = appStore.settingsStore, super(appStore: appStore) { @@ -237,9 +227,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = wallet.type == WalletType.monero || - wallet.type == WalletType.wownero || - wallet.type == WalletType.haven; + hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] + .contains(wallet.type); } static const String _cryptoNumberPattern = '0.00000000'; @@ -249,7 +238,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final FiatConversionStore fiatConversionStore; final SettingsStore _settingsStore; - List get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; + double? _fiatRate; + String _rawAmount = ''; + + List get currencies => + [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; String get buttonTitle { if (isElectrumWallet) { @@ -275,9 +268,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType get type => wallet.type; @computed - WalletAddressListItem get address { - return WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); - } + WalletAddressListItem get address => WalletAddressListItem( + address: wallet.walletAddresses.address, isPrimary: false); @computed PaymentURI get uri { @@ -321,8 +313,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final addressList = ObservableList(); if (wallet.type == WalletType.monero) { - final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = + monero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = + monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -338,8 +332,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (wallet.type == WalletType.wownero) { - final primaryAddress = wownero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = + wownero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = + wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -352,8 +348,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (wallet.type == WalletType.haven) { - final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first; - final addressItems = haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = + haven!.getSubaddressList(wallet).subaddresses.first; + final addressItems = + haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -367,7 +365,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (isElectrumWallet) { if (bitcoin!.hasSelectedSilentPayments(wallet)) { - final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { + final addressItems = + bitcoin!.getSilentPaymentAddresses(wallet).map((address) { final isPrimary = address.id == 0; return WalletAddressListItem( @@ -418,7 +417,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type == WalletType.litecoin && addressItems.length >= 1000) { // find the index of the last item with a txCount > 0 final addressItemsList = addressItems.toList(); - int index = addressItemsList.lastIndexWhere((item) => (item.txCount ?? 0) > 0); + int index = addressItemsList + .lastIndexWhere((item) => (item.txCount ?? 0) > 0); if (index == -1) { index = 0; } @@ -432,19 +432,22 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type == WalletType.ethereum) { final primaryAddress = ethereum!.getAddress(wallet); - addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem( + isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.polygon) { final primaryAddress = polygon!.getAddress(wallet); - addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem( + isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.solana) { final primaryAddress = solana!.getAddress(wallet); - addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem( + isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.nano) { @@ -458,18 +461,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type == WalletType.tron) { final primaryAddress = tron!.getAddress(wallet); - addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem( + isPrimary: true, name: null, address: primaryAddress)); } for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; - (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses + (addressList[i] as WalletAddressListItem).isHidden = wallet + .walletAddresses.hiddenAddresses .contains((addressList[i] as WalletAddressListItem).address); } for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; - (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses + (addressList[i] as WalletAddressListItem).isManual = wallet + .walletAddresses.manualAddresses .contains((addressList[i] as WalletAddressListItem).address); } @@ -487,7 +493,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo Future toggleHideAddress(WalletAddressListItem item) async { if (item.isHidden) { - wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address); + wallet.walletAddresses.hiddenAddresses + .removeWhere((element) => element == item.address); } else { wallet.walletAddresses.hiddenAddresses.add(item.address); } @@ -512,57 +519,58 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed String get accountLabel { - if (wallet.type == WalletType.monero) { - return monero!.getCurrentAccount(wallet).label; + switch (wallet.type) { + case WalletType.monero: + return monero!.getCurrentAccount(wallet).label; + case WalletType.wownero: + wownero!.getCurrentAccount(wallet).label; + case WalletType.haven: + return haven!.getCurrentAccount(wallet).label; + default: + return ''; } - - if (wallet.type == WalletType.wownero) { - return wownero!.getCurrentAccount(wallet).label; - } - - if (wallet.type == WalletType.haven) { - return haven!.getCurrentAccount(wallet).label; - } - return ''; } @computed - bool get hasAddressList => - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero || - wallet.type == WalletType.haven || - wallet.type == WalletType.bitcoinCash || - wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin; + bool get hasAddressList => [ + WalletType.monero, + WalletType.wownero, + WalletType.haven, + WalletType.bitcoinCash, + WalletType.bitcoin, + WalletType.litecoin + ].contains(wallet.type); @computed - bool get isElectrumWallet => - wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin || - wallet.type == WalletType.bitcoinCash; + bool get isElectrumWallet => [ + WalletType.bitcoin, + WalletType.litecoin, + WalletType.bitcoinCash + ].contains(wallet.type); @computed bool get isBalanceAvailable => isElectrumWallet; @computed bool get isReceivedAvailable => - wallet.type == WalletType.monero || wallet.type == WalletType.wownero; + [WalletType.monero, WalletType.wownero].contains(wallet.type); @computed bool get isSilentPayments => - wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet); + wallet.type == WalletType.bitcoin && + bitcoin!.hasSelectedSilentPayments(wallet); @computed bool get isAutoGenerateSubaddressEnabled => - _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled && + _settingsStore.autoGenerateSubaddressStatus != + AutoGenerateSubaddressStatus.disabled && !isSilentPayments; @computed bool get showAddManualAddresses => !isAutoGenerateSubaddressEnabled || - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero; + [WalletType.monero, WalletType.wownero].contains(wallet.type); List _baseItems; @@ -574,7 +582,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action Future setAddressType(dynamic option) async { - if (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) { + if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) { await bitcoin!.setAddressType(wallet, option); } } @@ -586,13 +594,15 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _baseItems.add(WalletAddressHiddenListHeader()); } - if (wallet.type == WalletType.monero || - wallet.type == WalletType.wownero || - wallet.type == WalletType.haven) { + if ([ + WalletType.monero, + WalletType.wownero, + WalletType.haven, + ].contains(wallet.type)) { _baseItems.add(WalletAccountListHeader()); } - if (wallet.type != WalletType.nano && wallet.type != WalletType.banano) { + if (![WalletType.nano, WalletType.banano].contains(wallet.type)) { _baseItems.add(WalletAddressListHeader()); } if (wallet.isEnabledAutoGenerateSubaddress) { @@ -603,11 +613,27 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action void selectCurrency(Currency currency) { selectedCurrency = currency; + + if (currency is FiatCurrency && _settingsStore.fiatCurrency != currency) { + final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); + + dev.log("Requesting Fiat rate for $cryptoCurrency-$currency"); + FiatConversionService.fetchPrice( + crypto: cryptoCurrency, + fiat: currency, + torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly, + ).then((value) { + dev.log("Received Fiat rate 1 $cryptoCurrency = $value $currency"); + _fiatRate = value; + _convertAmountToCrypto(); + }); + } } @action void changeAmount(String amount) { this.amount = amount; + this._rawAmount = amount; if (selectedCurrency is FiatCurrency) { _convertAmountToCrypto(); } @@ -618,11 +644,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo searchText = text; } + @action void _convertAmountToCrypto() { final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); + final fiatRate = + _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0); + + if (fiatRate <= 0.0) { + dev.log("Invalid Fiat Rate $fiatRate"); + amount = ''; + return; + } + try { - final crypto = - double.parse(amount.replaceAll(',', '.')) / fiatConversionStore.prices[cryptoCurrency]!; + final crypto = double.parse(_rawAmount.replaceAll(',', '.')) / fiatRate; final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); if (amount != cryptoAmountTmp) { amount = cryptoAmountTmp; From 5c2735be42db03bf7f4311389804fc7025dea7c1 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 18 Oct 2024 05:58:57 +0300 Subject: [PATCH 129/142] transaction description key id address (#1755) * transaction description key id+address * minor fix[skip ci] * fix backward compatibility * add monero primary address --- cw_core/lib/wallet_addresses.dart | 2 +- cw_evm/lib/evm_chain_wallet_addresses.dart | 3 +++ cw_monero/lib/monero_wallet_addresses.dart | 3 +++ cw_nano/lib/nano_wallet_addresses.dart | 3 +++ cw_solana/lib/solana_wallet_addresses.dart | 3 +++ cw_tron/lib/tron_wallet_addresses.dart | 3 +++ cw_wownero/lib/wownero_wallet_addresses.dart | 3 +++ lib/view_model/send/send_view_model.dart | 12 +++++++++--- lib/view_model/transaction_details_view_model.dart | 6 ++++-- 9 files changed, 32 insertions(+), 6 deletions(-) diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index ca488cfed..4324823c5 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -23,7 +23,7 @@ abstract class WalletAddresses { return _localAddress ?? address; } - String? get primaryAddress => null; + String get primaryAddress; String? _localAddress; diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index 4615d79ed..7dd501cc5 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -17,6 +17,9 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => address; + @override Future init() async { address = walletInfo.address; diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index c8a4448a4..ae78132d3 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -29,6 +29,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => getAddress(accountIndex: account?.id ?? 0, addressIndex: 0); + @override String get latestAddress { var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index cc532d2c7..e8eae7737 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -18,6 +18,9 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => address; + @observable NanoAccount? account; diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart index 97a76fb99..19eb91fa1 100644 --- a/cw_solana/lib/solana_wallet_addresses.dart +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -14,6 +14,9 @@ abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { @override String address; + @override + String get primaryAddress => address; + @override Future init() async { address = walletInfo.address; diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart index 35939de26..095f97fa9 100644 --- a/cw_tron/lib/tron_wallet_addresses.dart +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -17,6 +17,9 @@ abstract class TronWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => address; + @override Future init() async { address = walletInfo.address; diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index b2f9ec67a..eed81eb45 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -29,6 +29,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => getAddress(accountIndex: account?.id ?? 0, addressIndex: 0); + @override String get latestAddress { var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 334394a18..4064a1258 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -482,12 +482,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor nano!.updateTransactions(wallet); } + if (pendingTransaction!.id.isNotEmpty) { + + final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}'; _settingsStore.shouldSaveRecipientAddress ? await transactionDescriptionBox.add(TransactionDescription( - id: pendingTransaction!.id, recipientAddress: address, transactionNote: note)) - : await transactionDescriptionBox - .add(TransactionDescription(id: pendingTransaction!.id, transactionNote: note)); + id: descriptionKey, + recipientAddress: address, + transactionNote: note)) + : await transactionDescriptionBox.add(TransactionDescription( + id: descriptionKey, + transactionNote: note)); } state = TransactionCommitted(); diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index a96d70a90..1833a0f51 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -110,9 +110,11 @@ abstract class TransactionDetailsViewModelBase with Store { } catch (e) {} })); + final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final description = transactionDescriptionBox.values.firstWhere( - (val) => val.id == transactionInfo.txHash, - orElse: () => TransactionDescription(id: transactionInfo.txHash)); + (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey)); items.add(TextFieldListItem( title: S.current.note_tap_to_change, From 7faca38cfa52bcb3914c8b20cda291e4eefe594a Mon Sep 17 00:00:00 2001 From: Rafael Date: Fri, 18 Oct 2024 20:24:15 -0300 Subject: [PATCH 130/142] fix: sp input calc (#1756) --- cw_bitcoin/lib/electrum_wallet.dart | 45 ++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index ceaa2f088..527bef3ea 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -784,6 +784,7 @@ abstract class ElectrumWalletBase Future estimateTxForAmount( int credentialsAmount, List outputs, + List updatedOutputs, int feeRate, { int? inputsCount, String? memo, @@ -812,6 +813,7 @@ abstract class ElectrumWalletBase return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, @@ -824,19 +826,30 @@ abstract class ElectrumWalletBase } final changeAddress = await walletAddresses.getChangeAddress( - outputs: outputs, + outputs: updatedOutputs, utxoDetails: utxoDetails, ); final address = RegexUtils.addressTypeFromStr(changeAddress, network); + updatedOutputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, + )); outputs.add(BitcoinOutput( address: address, value: BigInt.from(amountLeftForChangeAndFee), isChange: true, )); + // calcFee updates the silent payment outputs to calculate the tx size accounting + // for taproot addresses, but if more inputs are needed to make up for fees, + // the silent payment outputs need to be recalculated for the new inputs + var temp = outputs.map((output) => output).toList(); int fee = await calcFee( utxos: utxoDetails.utxos, - outputs: outputs, + // Always take only not updated bitcoin outputs here so for every estimation + // the SP outputs are re-generated to the proper taproot addresses + outputs: temp, network: network, memo: memo, feeRate: feeRate, @@ -844,18 +857,25 @@ abstract class ElectrumWalletBase vinOutpoints: utxoDetails.vinOutpoints, ); + updatedOutputs.clear(); + updatedOutputs.addAll(temp); + if (fee == 0) { throw BitcoinTransactionNoFeeException(); } int amount = credentialsAmount; - final lastOutput = outputs.last; + final lastOutput = updatedOutputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; - print(amountLeftForChangeAndFee); - if (!_isBelowDust(amountLeftForChange)) { // Here, lastOutput already is change, return the amount left without the fee to the user's address. + updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); outputs[outputs.length - 1] = BitcoinOutput( address: lastOutput.address, value: BigInt.from(amountLeftForChange), @@ -864,6 +884,7 @@ abstract class ElectrumWalletBase ); } else { // If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change + updatedOutputs.removeLast(); outputs.removeLast(); // Still has inputs to spend before failing @@ -871,16 +892,18 @@ abstract class ElectrumWalletBase return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, + hasSilentPayment: hasSilentPayment, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, coinTypeToSpendFrom: coinTypeToSpendFrom, ); } final estimatedSendAll = await estimateSendAllTx( - outputs, + updatedOutputs, feeRate, memo: memo, coinTypeToSpendFrom: coinTypeToSpendFrom, @@ -913,10 +936,12 @@ abstract class ElectrumWalletBase if (spendingAllCoins) { throw BitcoinTransactionWrongBalanceException(); } else { + updatedOutputs.removeLast(); outputs.removeLast(); return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, @@ -1029,6 +1054,9 @@ abstract class ElectrumWalletBase : feeRate(transactionCredentials.priority!); EstimatedTxResult estimatedTx; + final updatedOutputs = + outputs.map((e) => BitcoinOutput(address: e.address, value: e.value)).toList(); + if (sendAll) { estimatedTx = await estimateSendAllTx( outputs, @@ -1042,6 +1070,7 @@ abstract class ElectrumWalletBase estimatedTx = await estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRateInt, memo: memo, hasSilentPayment: hasSilentPayment, @@ -1052,7 +1081,7 @@ abstract class ElectrumWalletBase if (walletInfo.isHardwareWallet) { final transaction = await buildHardwareWalletTransaction( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, publicKeys: estimatedTx.publicKeys, fee: BigInt.from(estimatedTx.fee), network: network, @@ -1092,7 +1121,7 @@ abstract class ElectrumWalletBase } else { txb = BitcoinTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, From 50825a62c194cdd97bf5414487d6d40b246c96a9 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Fri, 18 Oct 2024 17:05:48 -0700 Subject: [PATCH 131/142] Mweb enhancements 3 (#1744) * version 4.20.0 * update build numbers * UI updates and script fix for ios bundle identifier * disable mweb for desktop * change hardcoded ltc server ip address electrum connection enhancement * MWEB enhancements 2.0 (#1735) * additional logging and minor fixes * additional logging and minor fixes * addresses pt.1 * Allow Wallet Group Names to be the same as Wallet Names (#1730) * fix: Issues with imaging * fix: Allow group names to be the same as wallet names * fix: Bug with wallet grouping when a wallet is minimized * fix: Bug with wallet grouping when a wallet is minimized * logs of fixes and experimental changes, close wallet before opening next * save * fix icon * fixes * [skip ci] updates * [skip ci] updates * updates * minor optimizations * fix for when switching between wallets * [skip ci] updates * [skip ci] updates * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * mobx * mostly logging * stream fix pt.1 [skip ci] * updates * some fixes and enhancements * [skip ci] minor * potential partial fix for streamsink closed * fix stream sink closed errors * fix mweb logo colors * save * minor enhancements [skip ci] * save * experimental * minor * minor [skip ci] --------- Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: Omar Hatem * fix menu list removing from original list * detach sync status from mwebsyncstatus * minor * keep sync status in sync where necessary * minor * wip * appears to work? * updates * prevent mwebd from submitting non mweb transactions * fix unspent coins info not persisting for mweb coins + other minor fixes * [skip ci] minor * Polish MWEB card UI * make sure current chain tip is updated correctly [skip ci] * [skip ci] review fixes * [skip ci] detect mweb outputs more thoroughly (fix peg-in commit error) * fix change address on send ui * fix qr code scan issue * get segwit address for pegout even if mweb is selected on the receive screen [skip ci] * - Fix adding nodes twice - Fix mempool API parsing error * (potentially) fix duplicate tx history bug * [skip ci] fix bc1 address * don't show contacts prompt on pegin/out + potential unconfirmed balance fixes * [skip ci] minor cleanup * fix mweb input detection * fix showing mweb address for non-mweb transactions --------- Co-authored-by: OmarHatem Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: tuxpizza --- cw_bitcoin/lib/electrum.dart | 22 ++- cw_bitcoin/lib/electrum_wallet.dart | 69 +++++--- cw_bitcoin/lib/electrum_wallet_addresses.dart | 6 +- cw_bitcoin/lib/litecoin_wallet.dart | 150 ++++++++++++------ cw_bitcoin/lib/litecoin_wallet_addresses.dart | 16 +- .../lib/pending_bitcoin_transaction.dart | 6 + cw_mweb/lib/cw_mweb.dart | 7 +- lib/bitcoin/cw_bitcoin.dart | 22 ++- lib/entities/default_settings_migration.dart | 8 +- .../screens/dashboard/pages/balance_page.dart | 48 +++--- lib/src/screens/send/send_page.dart | 5 + lib/view_model/wallet_creation_vm.dart | 46 +++--- tool/configure.dart | 1 + 13 files changed, 266 insertions(+), 140 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 54ccaaef5..a8cb7ee9f 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -68,8 +68,8 @@ class ElectrumClient { try { await socket?.close(); - socket = null; } catch (_) {} + socket = null; try { if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { @@ -102,7 +102,8 @@ class ElectrumClient { return; } - _setConnectionStatus(ConnectionStatus.connected); + // use ping to determine actual connection status since we could've just not timed out yet: + // _setConnectionStatus(ConnectionStatus.connected); socket!.listen( (Uint8List event) { @@ -128,7 +129,7 @@ class ElectrumClient { print("SOCKET CLOSED!!!!!"); unterminatedString = ''; try { - if (host == socket?.address.host) { + if (host == socket?.address.host || socket == null) { _setConnectionStatus(ConnectionStatus.disconnected); socket?.destroy(); } @@ -178,7 +179,7 @@ class ElectrumClient { unterminatedString = ''; } } catch (e) { - print(e.toString()); + print("parse $e"); } } @@ -191,7 +192,7 @@ class ElectrumClient { try { await callWithTimeout(method: 'server.ping'); _setConnectionStatus(ConnectionStatus.connected); - } on RequestFailedTimeoutException catch (_) { + } catch (_) { _setConnectionStatus(ConnectionStatus.disconnected); } } @@ -431,7 +432,7 @@ class ElectrumClient { return subscription; } catch (e) { - print(e.toString()); + print("subscribe $e"); return null; } } @@ -470,7 +471,8 @@ class ElectrumClient { return completer.future; } catch (e) { - print(e.toString()); + print("callWithTimeout $e"); + rethrow; } } @@ -537,6 +539,12 @@ class ElectrumClient { onConnectionStatusChange?.call(status); _connectionStatus = status; _isConnected = status == ConnectionStatus.connected; + if (!_isConnected) { + try { + socket?.destroy(); + } catch (_) {} + socket = null; + } } void _handleResponse(Map response) { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 527bef3ea..893cff6f2 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -249,7 +250,7 @@ abstract class ElectrumWalletBase int? _currentChainTip; Future getCurrentChainTip() async { - if (_currentChainTip != null) { + if ((_currentChainTip ?? 0) > 0) { return _currentChainTip!; } _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; @@ -301,6 +302,7 @@ abstract class ElectrumWalletBase @action Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { + if (this is! BitcoinWallet) return; final chainTip = chainTipParam ?? await getUpdatedChainTip(); if (chainTip == height) { @@ -467,7 +469,7 @@ abstract class ElectrumWalletBase } } catch (e, stacktrace) { print(stacktrace); - print(e.toString()); + print("startSync $e"); syncStatus = FailedSyncStatus(); } } @@ -479,10 +481,10 @@ abstract class ElectrumWalletBase final response = await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended")); - final result = json.decode(response.body) as Map; - final slowFee = result['economyFee']?.toInt() ?? 0; - int mediumFee = result['hourFee']?.toInt() ?? 0; - int fastFee = result['fastestFee']?.toInt() ?? 0; + final result = json.decode(response.body) as Map; + final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; + int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; + int fastFee = (result['fastestFee'] as num?)?.toInt() ?? 0; if (slowFee == mediumFee) { mediumFee++; } @@ -491,7 +493,9 @@ abstract class ElectrumWalletBase } _feeRates = [slowFee, mediumFee, fastFee]; return; - } catch (_) {} + } catch (e) { + print(e); + } } final feeRates = await electrumClient.feeRates(network: network); @@ -571,7 +575,7 @@ abstract class ElectrumWalletBase await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); } catch (e, stacktrace) { print(stacktrace); - print(e.toString()); + print("connectToNode $e"); syncStatus = FailedSyncStatus(); } } @@ -826,8 +830,8 @@ abstract class ElectrumWalletBase } final changeAddress = await walletAddresses.getChangeAddress( + inputs: utxoDetails.availableInputs, outputs: updatedOutputs, - utxoDetails: utxoDetails, ); final address = RegexUtils.addressTypeFromStr(changeAddress, network); updatedOutputs.add(BitcoinOutput( @@ -1181,6 +1185,7 @@ abstract class ElectrumWalletBase hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, )..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { @@ -1370,7 +1375,7 @@ abstract class ElectrumWalletBase }); } - // Set the balance of all non-silent payment addresses to 0 before updating + // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) .forEach((addr) { @@ -1487,7 +1492,7 @@ abstract class ElectrumWalletBase await unspentCoinsInfo.deleteAll(keys); } } catch (e) { - print(e.toString()); + print("refreshUnspentCoinsInfo $e"); } } @@ -1831,7 +1836,7 @@ abstract class ElectrumWalletBase return historiesWithDetails; } catch (e) { - print(e.toString()); + print("fetchTransactions $e"); return {}; } } @@ -1905,7 +1910,9 @@ abstract class ElectrumWalletBase if (height > 0) { storedTx.height = height; // the tx's block itself is the first confirmation so add 1 - if (currentHeight != null) storedTx.confirmations = currentHeight - height + 1; + if ((currentHeight ?? 0) > 0) { + storedTx.confirmations = currentHeight! - height + 1; + } storedTx.isPending = storedTx.confirmations == 0; } @@ -1946,9 +1953,13 @@ abstract class ElectrumWalletBase } await getCurrentChainTip(); - transactionHistory.transactions.values.forEach((tx) async { - if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) { - tx.confirmations = await getCurrentChainTip() - tx.height! + 1; + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspents != null && + tx.unspents!.isNotEmpty && + tx.height != null && + tx.height! > 0 && + (_currentChainTip ?? 0) > 0) { + tx.confirmations = _currentChainTip! - tx.height! + 1; } }); @@ -1973,9 +1984,17 @@ abstract class ElectrumWalletBase await Future.wait(unsubscribedScriptHashes.map((address) async { final sh = address.getScriptHash(network); if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { - await _scripthashesUpdateSubject[sh]?.close(); + try { + await _scripthashesUpdateSubject[sh]?.close(); + } catch (e) { + print("failed to close: $e"); + } + } + try { + _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); + } catch (e) { + print("failed scripthashUpdate: $e"); } - _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { await updateUnspentsForAddress(address); @@ -2171,6 +2190,7 @@ abstract class ElectrumWalletBase @action void _onConnectionStatusChange(ConnectionStatus status) { + switch (status) { case ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || @@ -2182,19 +2202,26 @@ abstract class ElectrumWalletBase break; case ConnectionStatus.disconnected: - syncStatus = NotConnectedSyncStatus(); + if (syncStatus is! NotConnectedSyncStatus) { + syncStatus = NotConnectedSyncStatus(); + } break; case ConnectionStatus.failed: - syncStatus = LostConnectionSyncStatus(); + if (syncStatus is! LostConnectionSyncStatus) { + syncStatus = LostConnectionSyncStatus(); + } break; case ConnectionStatus.connecting: - syncStatus = ConnectingSyncStatus(); + if (syncStatus is! ConnectingSyncStatus) { + syncStatus = ConnectingSyncStatus(); + } break; default: } } void _syncStatusReaction(SyncStatus syncStatus) async { + print("SYNC_STATUS_CHANGE: ${syncStatus}"); if (syncStatus is SyncingSyncStatus) { return; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index ce1ff9713..6c10dc615 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -3,7 +3,7 @@ import 'dart:io' show Platform; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -267,7 +267,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -478,7 +478,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await saveAddressesInBox(); } catch (e) { - print(e.toString()); + print("updateAddresses $e"); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 29a2df48a..a09850f59 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -7,6 +7,7 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -95,6 +96,36 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); + reaction((_) => mwebSyncStatus, (status) async { + if (mwebSyncStatus is FailedSyncStatus) { + // we failed to connect to mweb, check if we are connected to the litecoin node: + late int nodeHeight; + try { + nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; + } catch (_) { + nodeHeight = 0; + } + + if (nodeHeight == 0) { + // we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us + } else { + // we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds: + await CwMweb.stop(); + await Future.delayed(const Duration(seconds: 5)); + startSync(); + } + } else if (mwebSyncStatus is SyncingSyncStatus) { + syncStatus = mwebSyncStatus; + } else if (mwebSyncStatus is SyncronizingSyncStatus) { + if (syncStatus is! SyncronizingSyncStatus) { + syncStatus = mwebSyncStatus; + } + } else if (mwebSyncStatus is SyncedSyncStatus) { + if (syncStatus is! SyncedSyncStatus) { + syncStatus = mwebSyncStatus; + } + } + }); } late final Bip32Slip10Secp256k1 mwebHd; late final Box mwebUtxosBox; @@ -105,6 +136,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { late bool mwebEnabled; bool processingUtxos = false; + @observable + SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); + List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; @@ -244,13 +278,24 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future startSync() async { print("startSync() called!"); - if (syncStatus is SyncronizingSyncStatus) { + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + if (!mwebEnabled) { + try { + // in case we're switching from a litecoin wallet that had mweb enabled + CwMweb.stop(); + } catch (_) {} + super.startSync(); return; } + + if (mwebSyncStatus is SyncronizingSyncStatus) { + return; + } + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); _syncTimer?.cancel(); try { - syncStatus = SyncronizingSyncStatus(); + mwebSyncStatus = SyncronizingSyncStatus(); try { await subscribeForUpdates(); } catch (e) { @@ -261,45 +306,32 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { _feeRatesTimer = Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); - if (!mwebEnabled) { - try { - // in case we're switching from a litecoin wallet that had mweb enabled - CwMweb.stop(); - } catch (_) {} - try { - await updateAllUnspents(); - await updateTransactions(); - await updateBalance(); - syncStatus = SyncedSyncStatus(); - } catch (e, s) { - print(e); - print(s); - syncStatus = FailedSyncStatus(); - } - return; - } - + print("START SYNC FUNCS"); await waitForMwebAddresses(); await processMwebUtxos(); await updateTransactions(); await updateUnspent(); await updateBalance(); - } catch (e) { - print("failed to start mweb sync: $e"); - syncStatus = FailedSyncStatus(error: "failed to start"); + print("DONE SYNC FUNCS"); + } catch (e, s) { + print("mweb sync failed: $e $s"); + mwebSyncStatus = FailedSyncStatus(error: "mweb sync failed: $e"); return; } _syncTimer = Timer.periodic(const Duration(milliseconds: 3000), (timer) async { - if (syncStatus is FailedSyncStatus) return; + if (mwebSyncStatus is FailedSyncStatus) { + _syncTimer?.cancel(); + return; + } final nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node if (nodeHeight == 0) { // we aren't connected to the ltc node yet - if (syncStatus is! NotConnectedSyncStatus) { - syncStatus = FailedSyncStatus(error: "Failed to connect to Litecoin node"); + if (mwebSyncStatus is! NotConnectedSyncStatus) { + mwebSyncStatus = FailedSyncStatus(error: "litecoin node isn't connected"); } return; } @@ -309,12 +341,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { try { if (resp.blockHeaderHeight < nodeHeight) { int h = resp.blockHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); } else if (resp.mwebHeaderHeight < nodeHeight) { int h = resp.mwebHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); } else if (resp.mwebUtxosHeight < nodeHeight) { - syncStatus = SyncingSyncStatus(1, 0.999); + mwebSyncStatus = SyncingSyncStatus(1, 0.999); } else { if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); @@ -325,6 +357,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { int txHeight = transaction.height ?? resp.mwebUtxosHeight; final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; if (transaction.confirmations == confirmations) continue; + if (transaction.confirmations == 0) { + updateBalance(); + } transaction.confirmations = confirmations; transactionHistory.addOne(transaction); } @@ -332,17 +367,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } // prevent unnecessary reaction triggers: - if (syncStatus is! SyncedSyncStatus) { + if (mwebSyncStatus is! SyncedSyncStatus) { // mwebd is synced, but we could still be processing incoming utxos: if (!processingUtxos) { - syncStatus = SyncedSyncStatus(); + mwebSyncStatus = SyncedSyncStatus(); } } return; } } catch (e) { print("error syncing: $e"); - syncStatus = FailedSyncStatus(error: e.toString()); + mwebSyncStatus = FailedSyncStatus(error: e.toString()); } }); } @@ -512,8 +547,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } _utxoStream = responseStream.listen((Utxo sUtxo) async { // we're processing utxos, so our balance could still be innacurate: - if (syncStatus is! SyncronizingSyncStatus && syncStatus is! SyncingSyncStatus) { - syncStatus = SyncronizingSyncStatus(); + if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { + mwebSyncStatus = SyncronizingSyncStatus(); processingUtxos = true; _processingTimer?.cancel(); _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { @@ -530,10 +565,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { value: sUtxo.value.toInt(), ); - // if (mwebUtxosBox.containsKey(utxo.outputId)) { - // // we've already stored this utxo, skip it: - // return; - // } + if (mwebUtxosBox.containsKey(utxo.outputId)) { + // we've already stored this utxo, skip it: + // but do update the utxo height if it's somehow different: + final existingUtxo = mwebUtxosBox.get(utxo.outputId); + if (existingUtxo!.height != utxo.height) { + print( + "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}"); + existingUtxo.height = utxo.height; + await mwebUtxosBox.put(utxo.outputId, existingUtxo); + } + return; + } await updateUnspent(); await updateBalance(); @@ -579,7 +622,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final height = await electrumClient.getCurrentBlockChainTip(); if (height == null || status.blockHeaderHeight != height) return; if (status.mwebUtxosHeight != height) return; // we aren't synced - int amount = 0; Set inputAddresses = {}; var output = convert.AccumulatorSink(); @@ -673,10 +715,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override @action Future updateAllUnspents() async { - // get ltc unspents: - await super.updateAllUnspents(); - if (!mwebEnabled) { + await super.updateAllUnspents(); return; } @@ -712,6 +752,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } mwebUnspentCoins.add(unspent); }); + + // copy coin control attributes to mwebCoins: + await updateCoins(mwebUnspentCoins); + // get regular ltc unspents (this resets unspentCoins): + await super.updateAllUnspents(); + // add the mwebCoins: unspentCoins.addAll(mwebUnspentCoins); } @@ -890,6 +936,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { tx.isMweb = mwebEnabled; if (!mwebEnabled) { + tx.changeAddressOverride = + await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false); return tx; } await waitForMwebAddresses(); @@ -913,12 +961,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { hasMwebOutput = true; break; } + if (output.address.toLowerCase().contains("mweb")) { + hasMwebOutput = true; + break; + } } - if (tx2.mwebBytes != null && tx2.mwebBytes!.isNotEmpty) { - hasMwebInput = true; + // check if mweb inputs are used: + for (final utxo in tx.utxos) { + if (utxo.utxo.scriptType == SegwitAddresType.mweb) { + hasMwebInput = true; + } } + bool isPegIn = !hasMwebInput && hasMwebOutput; + bool isRegular = !hasMwebInput && !hasMwebOutput; + tx.changeAddressOverride = await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular); if (!hasMwebInput && !hasMwebOutput) { tx.isMweb = false; return tx; @@ -971,7 +1030,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final addresses = {}; transaction.inputAddresses?.forEach((id) async { final utxo = mwebUtxosBox.get(id); - // await mwebUtxosBox.delete(id);// gets deleted in checkMwebUtxosSpent + await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent if (utxo == null) return; final addressRecord = walletAddresses.allAddresses .firstWhere((addressRecord) => addressRecord.address == utxo.address); @@ -990,6 +1049,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { print(e); print(s); if (e.toString().contains("commit failed")) { + print(e); throw Exception("Transaction commit failed (no peers responded), please try again."); } rethrow; diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 0a20665bf..6154a0ead 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; @@ -142,14 +143,15 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override - Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + Future getChangeAddress( + {List? inputs, List? outputs, bool isPegIn = false}) async { // use regular change address on peg in, otherwise use mweb for change address: - if (!mwebEnabled) { + if (!mwebEnabled || isPegIn) { return super.getChangeAddress(); } - if (outputs != null && utxoDetails != null) { + if (inputs != null && outputs != null) { // check if this is a PEGIN: bool outputsToMweb = false; bool comesFromMweb = false; @@ -161,14 +163,18 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with outputsToMweb = true; } } - // TODO: this doesn't respect coin control because it doesn't know which available inputs are selected - utxoDetails.availableInputs.forEach((element) { + + inputs.forEach((element) { + if (!element.isSending || element.isFrozen) { + return; + } if (element.address.contains("mweb")) { comesFromMweb = true; } }); bool isPegIn = !comesFromMweb && outputsToMweb; + if (isPegIn && mwebEnabled) { return super.getChangeAddress(); } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index c722dc14f..5ed84dbf4 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -24,6 +24,7 @@ class PendingBitcoinTransaction with PendingTransaction { this.isSendAll = false, this.hasTaprootInputs = false, this.isMweb = false, + this.utxos = const [], }) : _listeners = []; final WalletType type; @@ -36,7 +37,9 @@ class PendingBitcoinTransaction with PendingTransaction { final bool isSendAll; final bool hasChange; final bool hasTaprootInputs; + List utxos; bool isMweb; + String? changeAddressOverride; String? idOverride; String? hexOverride; List? outputAddresses; @@ -63,6 +66,9 @@ class PendingBitcoinTransaction with PendingTransaction { PendingChange? get change { try { final change = _tx.outputs.firstWhere((out) => out.isChange); + if (changeAddressOverride != null) { + return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); + } return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); } catch (_) { return null; diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index 39aa433cd..a1a592fb8 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -40,7 +40,7 @@ class CwMweb { } static Future _initializeClient() async { - print("initialize client called!"); + print("_initializeClient() called!"); final appDir = await getApplicationSupportDirectory(); const ltcNodeUri = "ltc-electrum.cakewallet.com:9333"; @@ -54,7 +54,7 @@ class CwMweb { log("Attempting to connect to server on port: $_port"); // wait for the server to finish starting up before we try to connect to it: - await Future.delayed(const Duration(seconds: 5)); + await Future.delayed(const Duration(seconds: 8)); _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () { _rpcClient = null; @@ -83,10 +83,13 @@ class CwMweb { log("Attempt $i failed: $e"); log('Caught grpc error: ${e.message}'); _rpcClient = null; + // necessary if the database isn't open: + await stop(); await Future.delayed(const Duration(seconds: 3)); } catch (e) { log("Attempt $i failed: $e"); _rpcClient = null; + await stop(); await Future.delayed(const Duration(seconds: 3)); } } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 91232792a..365d86be5 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -208,7 +208,7 @@ class CWBitcoin extends Bitcoin { {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) { final bitcoinWallet = wallet as ElectrumWallet; return bitcoinWallet.unspentCoins.where((element) { - switch(coinTypeToSpendFrom) { + switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: @@ -216,7 +216,6 @@ class CWBitcoin extends Bitcoin { case UnspentCoinType.any: return true; } - }).toList(); } @@ -399,19 +398,21 @@ class CWBitcoin extends Bitcoin { final history = await electrumClient.getHistory(sh); final balance = await electrumClient.getBalance(sh); - dInfoCopy.balance = balance.entries.first.value.toString(); + dInfoCopy.balance = balance.entries.firstOrNull?.value.toString() ?? "0"; dInfoCopy.address = address; dInfoCopy.transactionsCount = history.length; list.add(dInfoCopy); - } catch (e) { - print(e); + } catch (e, s) { + print("derivationInfoError: $e"); + print("derivationInfoStack: $s"); } } } // sort the list such that derivations with the most transactions are first: list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount)); + return list; } @@ -682,4 +683,15 @@ class CWBitcoin extends Bitcoin { return null; } } + + String? getUnusedSegwitAddress(Object wallet) { + try { + final electrumWallet = wallet as ElectrumWallet; + final segwitAddress = electrumWallet.walletAddresses.allAddresses + .firstWhere((element) => !element.isUsed && element.type == SegwitAddresType.p2wpkh); + return segwitAddress.address; + } catch (_) { + return null; + } + } } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 2b8c49631..198781cea 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -899,7 +899,9 @@ Future changeDefaultBitcoinNode( final newCakeWalletBitcoinNode = Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: false); - await nodeSource.add(newCakeWalletBitcoinNode); + if (!nodeSource.values.any((element) => element.uriRaw == newCakeWalletBitcoinUri)) { + await nodeSource.add(newCakeWalletBitcoinNode); + } if (needToReplaceCurrentBitcoinNode) { await sharedPreferences.setInt( @@ -931,6 +933,10 @@ Future _addBitcoinNode({ bool replaceExisting = false, bool useSSL = false, }) async { + bool isNodeExists = nodeSource.values.any((element) => element.uriRaw == nodeUri); + if (isNodeExists) { + return; + } const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; final currentBitcoinNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 708941023..84bdfd58b 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -17,7 +17,6 @@ import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/payment_request.dart'; @@ -843,7 +842,7 @@ class BalanceRowWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), child: Stack( children: [ if (currency == CryptoCurrency.ltc) @@ -851,17 +850,15 @@ class BalanceRowWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ Container( - padding: EdgeInsets.only(right: 16, top: 16), + padding: EdgeInsets.only(right: 16, top: 0), child: Column( children: [ Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), child: ImageIcon( AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), + color: Theme.of(context) + .extension()! + .assetTitleColor, size: 40, ), ), @@ -889,7 +886,6 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 24), Text( '${secondAvailableBalanceLabel}', textAlign: TextAlign.center, @@ -907,9 +903,9 @@ class BalanceRowWidget extends StatelessWidget { AutoSizeText( secondAvailableBalance, style: TextStyle( - fontSize: 20, + fontSize: 24, fontFamily: 'Lato', - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w900, color: Theme.of(context) .extension()! .assetTitleColor, @@ -918,15 +914,15 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - SizedBox(height: 4), + SizedBox(height: 6), if (!isTestnet) Text( '${secondAvailableFiatBalance}', textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, + fontSize: 16, fontFamily: 'Lato', - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w500, color: Theme.of(context) .extension()! .textColor, @@ -1019,7 +1015,6 @@ class BalanceRowWidget extends StatelessWidget { paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); } - Navigator.pushNamed( context, Routes.send, @@ -1030,11 +1025,10 @@ class BalanceRowWidget extends StatelessWidget { ); }, style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context) - .extension()! - .textFieldButtonIconColor + backgroundColor: Colors.grey.shade400 .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400, width: 0), + side: BorderSide(color: Colors.grey.shade400 + .withAlpha(50), width: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), @@ -1058,7 +1052,7 @@ class BalanceRowWidget extends StatelessWidget { style: TextStyle( color: Theme.of(context) .extension()! - .assetTitleColor, + .textColor, ), ), ], @@ -1074,13 +1068,12 @@ class BalanceRowWidget extends StatelessWidget { child: OutlinedButton( onPressed: () { final litecoinAddress = - bitcoin!.getAddress(dashboardViewModel.wallet); + bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); PaymentRequest? paymentRequest = null; - if (litecoinAddress.isNotEmpty) { + if ((litecoinAddress?.isNotEmpty ?? false)) { paymentRequest = PaymentRequest.fromUri( Uri.parse("litecoin:${litecoinAddress}")); } - Navigator.pushNamed( context, Routes.send, @@ -1091,11 +1084,10 @@ class BalanceRowWidget extends StatelessWidget { ); }, style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context) - .extension()! - .textFieldButtonIconColor + backgroundColor: Colors.grey.shade400 .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400, width: 0), + side: BorderSide(color: Colors.grey.shade400 + .withAlpha(50), width: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), @@ -1119,7 +1111,7 @@ class BalanceRowWidget extends StatelessWidget { style: TextStyle( color: Theme.of(context) .extension()! - .assetTitleColor, + .textColor, ), ), ], diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index ccf4a1dc4..d881d0341 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -28,6 +28,7 @@ import 'package:cake_wallet/utils/request_review_handler.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/send/output.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; @@ -508,6 +509,10 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); + + if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) { + newContactAddress = null; + } final successMessage = S.of(_dialogContext).send_success( sendViewModel.selectedCryptoCurrency.toString()); diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 90b4c5c3a..17a8d6d28 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -115,7 +115,9 @@ abstract class WalletCreationVMBase with Store { getIt.get().registerSyncTask(); _appStore.authenticationStore.allowed(); state = ExecutedSuccessfullyState(); - } catch (e, _) { + } catch (e, s) { + print("error: $e"); + print("stack: $s"); state = FailureState(e.toString()); } } @@ -194,31 +196,29 @@ abstract class WalletCreationVMBase with Store { final walletType = restoreWallet.type; var appStore = getIt.get(); var node = appStore.settingsStore.getCurrentNode(walletType); - - switch (walletType) { - case WalletType.bitcoin: - case WalletType.litecoin: - final derivationList = await bitcoin!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - passphrase: restoreWallet.passphrase, - ); + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + final derivationList = await bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + if (derivationList.firstOrNull?.transactionsCount == 0 && derivationList.length > 1) + return []; + return derivationList; - if (derivationList.first.transactionsCount == 0 && derivationList.length > 1) return []; - - return derivationList; - - case WalletType.nano: - return nanoUtil!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - default: - break; - } - return list; + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; } WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); diff --git a/tool/configure.dart b/tool/configure.dart index ce079dd29..704b47526 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -231,6 +231,7 @@ abstract class Bitcoin { Future setMwebEnabled(Object wallet, bool enabled); bool getMwebEnabled(Object wallet); String? getUnusedMwebAddress(Object wallet); + String? getUnusedSegwitAddress(Object wallet); } """; From 34166c3a11af78c876f983131e2ea12809330692 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 21 Oct 2024 13:03:53 +0300 Subject: [PATCH 132/142] v4.20.0 (#1759) * new version * fix android manifest [skip ci] * fix sp scanning [skip ci] --- android/app/src/main/AndroidManifestBase.xml | 2 +- assets/text/Monerocom_Release_Notes.txt | 7 +++---- assets/text/Release_Notes.txt | 15 ++++++--------- cw_bitcoin/lib/electrum.dart | 6 +++--- cw_bitcoin/lib/litecoin_wallet.dart | 2 -- cw_core/lib/wallet_addresses.dart | 2 +- lib/src/screens/dashboard/pages/balance_page.dart | 2 +- scripts/android/app_env.sh | 4 ++-- scripts/ios/app_env.sh | 4 ++-- scripts/linux/app_env.sh | 2 +- scripts/macos/app_env.sh | 4 ++-- 11 files changed, 22 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 1d0e2c93c..98bbb1236 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -14,7 +14,7 @@ - + diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 2f79dd5c6..613ea4281 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,4 +1,3 @@ -Monero enhancements for sending and address generation -StealthEx -LetsExchange -Visual enhancements and bug fixes \ No newline at end of file +Monero enhancements +Introducing StealthEx and LetxExchange +Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 868ae7954..d359d6e8a 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,9 +1,6 @@ -Add Litecoin MWEB -Wallet groups (same seed, multiple wallets) -Silent Payments enhancements -Monero enhancements for sending and address generation -StealthEx -LetsExchange -Replace-By-Fee improvements -ERC20 tokens potential scam detection -Visual enhancements and bug fixes \ No newline at end of file +Added Litecoin MWEB +Added wallet groups +Silent Payment enhancements for speed & reliability +Monero enhancements +Introducing StealthEx and LetxExchange +Additional ERC20 tokens scam detection \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index a8cb7ee9f..a18c038fa 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -423,7 +423,7 @@ class ElectrumClient { BehaviorSubject? subscribe( {required String id, required String method, List params = const []}) { try { - if (socket == null || !isConnected) { + if (socket == null) { return null; } final subscription = BehaviorSubject(); @@ -439,7 +439,7 @@ class ElectrumClient { Future call( {required String method, List params = const [], Function(int)? idCallback}) async { - if (socket == null || !isConnected) { + if (socket == null) { return null; } final completer = Completer(); @@ -455,7 +455,7 @@ class ElectrumClient { Future callWithTimeout( {required String method, List params = const [], int timeout = 5000}) async { try { - if (socket == null || !isConnected) { + if (socket == null) { return null; } final completer = Completer(); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index a09850f59..afa8cc388 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -7,7 +7,6 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; -import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -425,7 +424,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool? usingElectrs, }) async { _syncTimer?.cancel(); - int oldHeight = walletInfo.restoreHeight; await walletInfo.updateRestoreHeight(height); // go through mwebUtxos and clear any that are above the new restore height: diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 4324823c5..714d229d9 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -23,7 +23,7 @@ abstract class WalletAddresses { return _localAddress ?? address; } - String get primaryAddress; + String get primaryAddress => address; String? _localAddress; diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 84bdfd58b..478f1a3c3 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -1061,7 +1061,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), ), - SizedBox(width: 32), + SizedBox(width: 24), Expanded( child: Semantics( label: S.of(context).litecoin_mweb_pegout, diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 428891b2c..22fa7ef20 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -16,14 +16,14 @@ APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.17.0" -MONERO_COM_BUILD_NUMBER=102 +MONERO_COM_BUILD_NUMBER=103 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.20.0" -CAKEWALLET_BUILD_NUMBER=230 +CAKEWALLET_BUILD_NUMBER=232 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index d976f07fd..066b752cc 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -14,12 +14,12 @@ APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.17.0" -MONERO_COM_BUILD_NUMBER=100 +MONERO_COM_BUILD_NUMBER=101 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.20.0" -CAKEWALLET_BUILD_NUMBER=269 +CAKEWALLET_BUILD_NUMBER=276 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index aaeb2bc2a..f1616d7c5 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -15,7 +15,7 @@ fi CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.10.0" -CAKEWALLET_BUILD_NUMBER=35 +CAKEWALLET_BUILD_NUMBER=36 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 af2dc1322..9c5fef1ff 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="1.7.0" -MONERO_COM_BUILD_NUMBER=33 +MONERO_COM_BUILD_NUMBER=34 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.13.0" -CAKEWALLET_BUILD_NUMBER=90 +CAKEWALLET_BUILD_NUMBER=92 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 0a1e32c363fbf1a654b459665cfc7cbc6643080e Mon Sep 17 00:00:00 2001 From: omahs <73983677+omahs@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:29:00 +0200 Subject: [PATCH 133/142] Fix typos (#1760) * fix typos * fix typos * fix typos * fix typos --- build-guide-linux.md | 4 ++-- how_to_add_new_wallet_type.md | 6 +++--- howto-build-ios.md | 4 ++-- howto-build-windows.md | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build-guide-linux.md b/build-guide-linux.md index e0158945b..50ecc76fe 100644 --- a/build-guide-linux.md +++ b/build-guide-linux.md @@ -15,7 +15,7 @@ These steps will help you configure and execute a build of CakeWallet from its s ### 1. Installing Package Dependencies -CakeWallet requires some packages to be install on your build system. You may easily install them on your build system with the following command: +CakeWallet requires some packages to be installed on your build system. You may easily install them on your build system with the following command: `$ sudo apt install build-essential cmake pkg-config git curl autoconf libtool` @@ -145,7 +145,7 @@ Path to executable file will be: # Flatpak -For package the built application into flatpak you need fistly to install `flatpak` and `flatpak-builder`: +For package the built application into flatpak you need firstly to install `flatpak` and `flatpak-builder`: `$ sudo apt install flatpak flatpak-builder` diff --git a/how_to_add_new_wallet_type.md b/how_to_add_new_wallet_type.md index 917e87cf4..d71e181e0 100644 --- a/how_to_add_new_wallet_type.md +++ b/how_to_add_new_wallet_type.md @@ -5,7 +5,7 @@ **N:B** Throughout this guide, `walletx` refers to the specific wallet type you want to add. If you're adding `BNB` to CakeWallet, then `walletx` for you here is `bnb`. **Core Folder/Files Setup** -- Idenitify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc +- Identify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc - Add a new entry to `WalletType` class in `cw_core/wallet_type.dart`. - Fill out the necessary information in the various functions in the files, concerning the wallet name, the native currency type, symbol etc. - Go to `cw_core/lib/currency_for_wallet_type.dart`, in the `currencyForWalletType` function, add a case for `walletx`, returning the native cryptocurrency for `walletx`. @@ -144,7 +144,7 @@ You can add as many node entries as desired. } } -- Next, we’ll write the function to change walletX current node to default. An handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id. +- Next, we’ll write the function to change walletX current node to default. A handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id. Future changeWalletXCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { @@ -228,7 +228,7 @@ Now you can run the codebase and successfully create a wallet for type walletX s **Balance Screen** - Go to `lib/view_model/dashboard/balance_view_model.dart` -- Modify the function to adjust the way the balance is being display on the app: `isHomeScreenSettingsEnabled` +- Modify the function to adjust the way the balance is being displayed on the app: `isHomeScreenSettingsEnabled` - Add a case to the `availableBalanceLabel` getter to modify the text being displayed (Available or confirmed) - Same for `additionalBalanceLabel` - Next, go to `lib/reactions/fiat_rate_update.dart` diff --git a/howto-build-ios.md b/howto-build-ios.md index 418fbc96b..544d4359e 100644 --- a/howto-build-ios.md +++ b/howto-build-ios.md @@ -57,7 +57,7 @@ Proceed into the source code before proceeding with the next steps: ### 7. Execute Build & Setup Commands for Cake Wallet -We need to generate project settings like app name, app icon, package name, etc. For this need to setup environment variables and configure project files. +We need to generate project settings like app name, app icon, package name, etc. For this, we need to setup environment variables and configure project files. Please pick what app you want to build: cakewallet or monero.com. @@ -92,7 +92,7 @@ Then we need to generate localization files and mobx models. `$ flutter build ios --release` -Then you can open `ios/Runner.xcworkspace` with Xcode and you can to archive the application. +Then you can open `ios/Runner.xcworkspace` with Xcode and you can archive the application. Or if you want to run to connected device: diff --git a/howto-build-windows.md b/howto-build-windows.md index 796cb3cc8..504f8f785 100644 --- a/howto-build-windows.md +++ b/howto-build-windows.md @@ -22,7 +22,7 @@ Then install `Desktop development with C++` packages via Visual Studio 2022, or - `C++ 2022 Redistributable Update` - `C++ core desktop features` - `MVC v143 - VS 2022 C++ x64/x86 build tools` -- `C++ CMake tools for Windwos` +- `C++ CMake tools for Windows` - `Testing tools core features - Build Tools` - `C++ AddressSanitizer`. @@ -38,7 +38,7 @@ For building monero dependencies, it is required to install Windows WSL (https:/ ### 5. Pull Cake Wallet source code -You can downlaod CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git: +You can download CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git: `$ git clone https://github.com/cake-tech/cake_wallet.git --branch MrCyjaneK-cyjan-monerodart` OR you can download it as [Zip archive](https://github.com/cake-tech/cake_wallet/archive/refs/heads/MrCyjaneK-cyjan-monerodart.zip) @@ -52,6 +52,6 @@ For that you need to run the shell (bash - typically same named utility should b To configure the application, open the directory where you have downloaded or unarchived Cake Wallet sources and run `cakewallet.bat`. Or if you used WSL and have active shell session you can run `$ ./cakewallet.sh` script in `scripts/windows` which will run `cakewallet.bat` in WSL. -After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contains `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application. +After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contain `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application. Copyright (c) 2024 Cake Labs LLC. From 20efba43cb2e335bd75f88d05c6c00649a3239f0 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 22 Oct 2024 15:23:42 +0300 Subject: [PATCH 134/142] enable ssl for btc-electrum (#1765) --- assets/bitcoin_electrum_server_list.yml | 1 + lib/entities/default_settings_migration.dart | 12 ++++++++++++ lib/main.dart | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/assets/bitcoin_electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml index 305db38f5..20a28cd24 100644 --- a/assets/bitcoin_electrum_server_list.yml +++ b/assets/bitcoin_electrum_server_list.yml @@ -3,6 +3,7 @@ useSSL: true - uri: btc-electrum.cakewallet.com:50002 + useSSL: true isDefault: true - uri: electrs.cakewallet.com:50001 diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 198781cea..af6f1ce78 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -256,6 +256,9 @@ Future defaultSettingsMigration( await _addSethNode(nodes, sharedPreferences); await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); break; + case 42: + updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences); + break; default: break; } @@ -270,6 +273,15 @@ Future defaultSettingsMigration( await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); } +void updateBtcElectrumNodeToUseSSL(Box nodes, SharedPreferences sharedPreferences) { + final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri); + + if (btcElectrumNode != null) { + btcElectrumNode.useSSL = true; + btcElectrumNode.save(); + } +} + void _deselectQuantex(SharedPreferences sharedPreferences) { final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") diff --git a/lib/main.dart b/lib/main.dart index 7237de8b9..2b5bef06c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -192,7 +192,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 41, + initialMigrationVersion: 42, ); } From 3898208a6aad9f41b38eaca210c66178a38156f4 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 22 Oct 2024 16:46:25 +0300 Subject: [PATCH 135/142] fix send all (#1766) * shouldn't matter, but still investigating why this fixes it * got it, but this change is just for consistency [skip ci] --- cw_bitcoin/lib/electrum_wallet.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 893cff6f2..4d127c248 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1063,7 +1063,7 @@ abstract class ElectrumWalletBase if (sendAll) { estimatedTx = await estimateSendAllTx( - outputs, + updatedOutputs, feeRateInt, memo: memo, credentialsAmount: credentialsAmount, @@ -1115,7 +1115,7 @@ abstract class ElectrumWalletBase if (network is BitcoinCashNetwork) { txb = ForkedTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, From 68b18f66bd6e604d09602c5ee26901837f9c70ba Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 22 Oct 2024 17:40:49 +0300 Subject: [PATCH 136/142] new versions (#1767) --- assets/text/Release_Notes.txt | 3 ++- scripts/android/app_env.sh | 4 ++-- scripts/ios/app_env.sh | 4 ++-- scripts/linux/app_env.sh | 4 ++-- scripts/macos/app_env.sh | 4 ++-- scripts/windows/build_exe_installer.iss | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index d359d6e8a..61aafb6e4 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -3,4 +3,5 @@ Added wallet groups Silent Payment enhancements for speed & reliability Monero enhancements Introducing StealthEx and LetxExchange -Additional ERC20 tokens scam detection \ No newline at end of file +Additional ERC20 tokens scam detection +Bug fixes \ No newline at end of file diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 22fa7ef20..8beffbcc2 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -22,8 +22,8 @@ MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.20.0" -CAKEWALLET_BUILD_NUMBER=232 +CAKEWALLET_VERSION="4.20.1" +CAKEWALLET_BUILD_NUMBER=233 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 066b752cc..bc3b39747 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -18,8 +18,8 @@ MONERO_COM_BUILD_NUMBER=101 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.20.0" -CAKEWALLET_BUILD_NUMBER=276 +CAKEWALLET_VERSION="4.20.1" +CAKEWALLET_BUILD_NUMBER=277 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index f1616d7c5..2dabf083b 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.10.0" -CAKEWALLET_BUILD_NUMBER=36 +CAKEWALLET_VERSION="1.10.1" +CAKEWALLET_BUILD_NUMBER=37 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 9c5fef1ff..026ea034b 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -21,8 +21,8 @@ MONERO_COM_BUILD_NUMBER=34 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.13.0" -CAKEWALLET_BUILD_NUMBER=92 +CAKEWALLET_VERSION="1.13.1" +CAKEWALLET_BUILD_NUMBER=93 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index 33cd11292..f65900cca 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.1.0" +#define MyAppVersion "0.1.1" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" From e04185a7c1c96f2bd155a5a491d21b80a0f9b24f Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Tue, 22 Oct 2024 21:06:08 +0300 Subject: [PATCH 137/142] upload x86 arch to artifacts [skip ci] --- .github/workflows/pr_test_build_android.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 925f4e00d..c9021fac0 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -224,6 +224,7 @@ jobs: cd /opt/android/cake_wallet/build/app/outputs/flutter-apk mkdir test-apk cp app-arm64-v8a-release.apk test-apk/${{env.BRANCH_NAME}}.apk + cp app-x86_64-release.apk test-apk/${{env.BRANCH_NAME}}_x86.apk - name: Upload Artifact uses: kittaakos/upload-artifact-as-is@v0 From 68926c0a337700165b807e76f34a31c52a9d0b79 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 23 Oct 2024 17:38:31 +0200 Subject: [PATCH 138/142] Cw 679 add ledger litecoin support (#1565) * Add Litecoin Hardware Wallet Creation * Add Litecoin Hardware Wallet Creation * Fix Bitcoin not sending on Ledger * Fixes to sending LTC using Ledger * CW-679 Fix merge conflicts * CW-679 Fix merge conflicts * CW-679 Minor fixes * CW-679 Add derivation Path of change address * ledger flutter plus refactoring * ledger flutter plus refactoring * ledger flutter plus refactoring * Ups :| * Ups :| I forgot USB * Handle BT Off * Fix Issue with A14 and USB * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Pls work * Pls work * Pls work * Pls work * Fix overpopulation * Fix ble device detection and support for Stax and Flex * clean up pubspec * clean up * MWeb merge fix * MWeb merge fix * Fix Merge conflicts * Fix Requested changes --- assets/images/hardware_wallet/ledger_flex.png | Bin 0 -> 1777 bytes .../images/hardware_wallet/ledger_nano_s.png | Bin 0 -> 2639 bytes .../images/hardware_wallet/ledger_nano_x.png | Bin 0 -> 2634 bytes assets/images/hardware_wallet/ledger_stax.png | Bin 0 -> 1612 bytes assets/images/ledger_nano.png | Bin 1463 -> 0 bytes .../lib/bitcoin_hardware_wallet_service.dart | 21 +-- cw_bitcoin/lib/bitcoin_wallet.dart | 57 ++++---- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 1 + cw_bitcoin/lib/electrum_wallet.dart | 69 ++++++--- cw_bitcoin/lib/electrum_wallet_addresses.dart | 21 ++- .../lib/litecoin_hardware_wallet_service.dart | 46 ++++++ cw_bitcoin/lib/litecoin_wallet.dart | 102 +++++++++++-- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 29 ++-- cw_bitcoin/lib/litecoin_wallet_service.dart | 23 ++- cw_bitcoin/lib/psbt_transaction_builder.dart | 4 - cw_bitcoin/pubspec.lock | 134 +++++++++++------- cw_bitcoin/pubspec.yaml | 19 +-- .../lib/src/bitcoin_cash_wallet.dart | 1 + .../src/bitcoin_cash_wallet_addresses.dart | 1 + .../lib/hardware/device_connection_type.dart | 1 + .../evm_chain_hardware_wallet_service.dart | 18 +-- .../evm_chain_transaction_credentials.dart | 1 - cw_evm/lib/evm_ledger_credentials.dart | 57 ++++---- cw_evm/pubspec.yaml | 9 +- lib/bitcoin/cw_bitcoin.dart | 24 +++- .../hardware_wallet_device.dart | 65 +++++++++ lib/ethereum/cw_ethereum.dart | 16 +-- lib/main.dart | 14 +- lib/polygon/cw_polygon.dart | 15 +- .../connect_device/connect_device_page.dart | 120 ++++++++++------ .../connect_device/debug_device_page.dart | 112 +++++++++------ .../connect_device/widgets/device_tile.dart | 2 +- .../screens/dashboard/pages/balance_page.dart | 2 +- .../screens/restore/restore_options_page.dart | 4 +- lib/src/screens/send/send_page.dart | 11 +- .../dashboard/dashboard_view_model.dart | 2 +- .../hardware_wallet/ledger_view_model.dart | 109 ++++++++++---- lib/view_model/send/send_view_model.dart | 19 ++- .../wallet_hardware_restore_view_model.dart | 13 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec_base.yaml | 9 +- res/values/strings_de.arb | 74 +++++----- tool/configure.dart | 16 ++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 45 files changed, 842 insertions(+), 405 deletions(-) create mode 100644 assets/images/hardware_wallet/ledger_flex.png create mode 100644 assets/images/hardware_wallet/ledger_nano_s.png create mode 100644 assets/images/hardware_wallet/ledger_nano_x.png create mode 100644 assets/images/hardware_wallet/ledger_stax.png delete mode 100644 assets/images/ledger_nano.png create mode 100644 cw_bitcoin/lib/litecoin_hardware_wallet_service.dart create mode 100644 lib/entities/hardware_wallet/hardware_wallet_device.dart diff --git a/assets/images/hardware_wallet/ledger_flex.png b/assets/images/hardware_wallet/ledger_flex.png new file mode 100644 index 0000000000000000000000000000000000000000..fa39f241fccf109565a28f66c090bf57ca274fd0 GIT binary patch literal 1777 zcmVPx*t4TybRCr$Poo$FzRT#(r&s~*jgAA8~ie9AXgwQf<` z8}8g$1C7X?NwLTX9r_R^>D=*~Nl*lS%A7l!(W{1qRG&;1F<6t<`^2uhIxqKS?mhR+ znVmiJbj;={ z0FtS#w>fa`137&Ls1M+))cSxX9Pnwdae6_I^ZhsbawprVr!5iC|J1fV_td!$ z4;|V(DNCs|0LfK_0cI|Nu~e3LS>x742H}|FAU#$#e~@8M3V>uLzm6I2%Mg5N%%&ic z8dc|r@m!^Bej4|jVh1pgDZE0=@5U{(6UHOLno7ys9jBI90W43A_B%w?V1ZIsYq0s~ zE&esw=|55)`8oO!^*v()a8GLU)xC7ZF92@x6*ud9##b=#BQc+8YVQCr;+o>jI38m% z(PB8b7laipT0CxB=3One4Qv)0fJC~m4dB-UES@C7rVA6l?;kr*oCs9c#s>GV-rBq9 z{A7keZvnW$w|r$Du9kCK6FTnTqFfMhzqlR-mn?|OCvfQ$3PQ2mWA zj~$GS2kr8(=hH#AUgA}d-hwXq)gj~dvC{C=bL5tP;sCn53=NTl&)Sw*^9Fcp3^ahC zTM>;5%%d9Rh%al21JGL#nE35#DfdQeYrejf1`reh0gogd#zv8E2gCu)=Nu5jy)F$P zC;}ueFbzNhh?2V6*T=UE)mxB!5E_7dANyL+05kx956E62iv17S^V3z3NEKvI(cUNa z=AQ;20-*D&AfsrPhP(IO(9Q?Y<*%h^0P<6`PQvnitgizU`&a{*Qvm8dLJ`k#x(w~^ zGE{tpsE&@yN!aCKu&#pS`v1tk}mIfCeDo0oi+`fV5b;%ATJtLviehu5E4L%bzH@wAa@GtvOMo)f|Zc8gxC3oAd*jAow~7>m(esM%4b@eG8hoFQjvT zd^mF?2h1GU)aZ{GR)~BT`&;OI00LjgJd`QiJ?UVDK~A3vM>}ks2JmIIoIUPk-|w)F zIKBdvnuH0?u8K9{dT8JDzcwLyM}^}XZ8FokYB~FXo0i%LXgsc_GXO-0fS}WMLqiEZ z#=-3>s_y^TTwuQA+j8ziW5YQe0r1EH1L?vh0&foOXsI1&#I~|c2hetY4)FPe$16k= z>HJiFjsfi&zehxF2i*3XEezu$IA+vs3J0l0I z$`|3x+=^)NunwsJ!XumG03syesl>+2Yn#I_;_{8?^xAHo+hv6Lg^?p=3%XRUc_iU5 z2jPX8j8I!It=;&Us!9N!M^9=1LhDz&^Be~77=im;JwQFxN@@5rm(uQH9~u8IEGmj1 zt#0Fb^a&$DI0u9=rrS1qKm$;`Dl`BMK(PlDo7WMb0Vwu>V)HrzGyugOP;CBx3ot01 ToY=F<00000NkvXXu0mjfR%KGE literal 0 HcmV?d00001 diff --git a/assets/images/hardware_wallet/ledger_nano_s.png b/assets/images/hardware_wallet/ledger_nano_s.png new file mode 100644 index 0000000000000000000000000000000000000000..02777aeb69b2e436d2b9bd96c3b5f39a773b7def GIT binary patch literal 2639 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 literal 0 HcmV?d00001 diff --git a/assets/images/hardware_wallet/ledger_stax.png b/assets/images/hardware_wallet/ledger_stax.png new file mode 100644 index 0000000000000000000000000000000000000000..06c9c848e8336bacbe4378b5e5999604393821e6 GIT binary patch literal 1612 zcmV-S2DABzP)Px*21!IgRCr$Pon43I}_78*`QruT<7~(O#p^{uMC_A;~WhcKbIC#6~BG-ECNOo+54F2Fo0X_2W7dy z!sG7R=Dz?=7#C^Z;?dMOVW(P}^8`R_JohLuj`=YFsXnYJuqH+o-ZC?#D#g^hQuf=~ z5eG0jHob#489xEo;mmnwvvpk{XI6V|Ww~YItBWunKV6*oqi1_<42T1WC1xH2aZ%c^ zC;(^7tCBJEnN8So*Vo0Dt@I@Rk=6&rug|k80B!l8!R7v6hMw7Ys7BcPw!Izk_t4~@;ilU1SkLxr*jvkH3GU2P4bfP0~nu~ z1M}WiqV#(eWceJo@gKlv2Ak={;>7n-(B;>F@!2`B8C3vP-5Hpz6w)aP2=V|zdMCW0 z40W`Z#o%Wuh0F)NLXZa#?G&_8@88RX^fd~gR|Oi&Q2RBYuJw??H-58_`K1>ZJdc2l z4^kqq2-j)N2H##;$eiwL02`fC0L`d8i2!Ea+sd#3%q{luW?XX_YX3B?KO-QSn`dB8 zOS0kEjB`{4S+ES|*kUoW)L$uRK+4c6fD0U3Jf;A8nSf2#+HOTOwAKg^m43}w_yG*g zjTriJ5w?3PA`E4yo&QY%$Hr~t;=ylw2}Joav|$mhd!tROpe9!G00trjt=%82GT(fv zm^s_m5m4t`v?5@+Tu5*2MSSa8`3M-0`v78krBqD4(SHC2AO#UHT`Hs}dllcC2&e;4 zzn2n*D#-qkBp+uoCoB2M?CJq6%WFzMzBKw43k_Syp=Yu`ZpD^^+s4Nev0%?xrm!81LE2+#=dc1F9k9*IS`?GLwV8LDMyr-+D71n6d5C)U;f z>muxU5hfUfFh7t9&s!0Gu3;JhO%aei4$vdb`J*b;;c_9h-%)OH*hn5g*uSEb%vnEO zd~Iv}cz?N&vChR2`^W>RJ(LgOBHZcl31+&#QcQg*roJx1Yf|3()YQn|%OkS{ykMir z%3tR%-*)!oODp{iz$OLti5?1$w-`e{k=Jt;$7{nkQPqp|-LtFBBs zxX4^JZ&~_u;ztKfT{RJ314fhCDF%uD#=ff&dC-`MkCgJ6Cp|DNe4t70)Lkbkv^!(gnzhR2L!mmz%@Qx3$UX?7 zlb3{S8n+W62n^nMyr2kA$3_Sr51X5x^=jtV{19IQYKH$$MehT+%JHCGH*^BhrBTL7 z8lu;{I5Vu3C!PVYI@>k&=G7O5&D{q0dJxwgjI(~ytaaY3^;fVTlZ`(K^hvpR@Ham_ z@f3ic0|{$i3Ls!|3P1q_tU$osYXm5OfE5V1dyN1E5U>IPcmE$44V#X~Kndvp0000< KMNUMnLSTZg0^5E7 literal 0 HcmV?d00001 diff --git a/assets/images/ledger_nano.png b/assets/images/ledger_nano.png deleted file mode 100644 index bb61ba1750728b5584680110bd90727c3e195eca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1463 zcmV;o1xWgdP)@~0drDELIAGL9O(c600d`2O+f$vv5yPXg70CCgwzCt2)>6E5>gQmA^0Admtb5#jNp4@T7ppl2?XCG zvl5I6&<#GdX&}HZO+ObEO*1LMh=4G`-``rVKYsMEJvYB3Y?P3=fUv>$ez#RZq5`4@ z-@`@;i3x}sd=J|sBqAWG;Ct94A+~^|gYRLBgjfPh1>eI436TYu4!%d$B}5WX{=8Yr zA+%KO;s)O%%Mv09c=gh?Iz{7ZP@ANJ?~zpr4n%;;XqSUOPd4}-hqO(kZL8W*oBBJ{ zMZ!5Ck^qg(A{ZiT5+Zd$ugrgVMbQNr?FO>c9Yq@h-ds8oLevz^Sb_$T1U&xZVRsr1 zJ_`~euLGv#;Lp2+SSw+c5TDq&AeIo1M6#hQAv%fG16V>d60L}_gzzL@6J`nFFmhJ} zOVG!N00~Rb!~R z=8tYgi|P$ufI;;41FB$<)CEZN4j!Mt6zm5I=z&2dB0!=#cvm=Wn1cNP7usNuRL6i( z{Q6>fSHthapnDIPE+Dk~eQYH0ZX+!L z2|n?jEf6;2ickuW4`UQu@fR>Yh@K;$@%xXrBlQl-R74&fqsgfA56-%Xccb14uM{BA zEd!$@>hlN_OU7jpDdFAgcUHrECl4{80i;VglQv`=1SnB`qAa@`Ra+Nfqg-vabO+{G z0%Y(Hw%2ZeB~+H1H4?EQh4{C^*+M})pda1`4Y&&mflN)GoE%-Dv;2NwLjj92!wVY3 zx&pGs*_Hla^-IN=(UotfRhJdA)yhhfx+DLx=`g+CR{c-D0*1)NW}$fDKS5JUI)iIe zoY@eL*r_akvqOD)X|#R+Z1%P6uPu)qOD0bx4arV#hN?UaZ{&lp?Hykk0rm)b+g1Q zASCk4FU(xHOA)RGweuS@MqvS(3<6)6sSdgwR4lNlSihG>JwWD3C|UIYvQG&$3o@rO zCRgk1m&CmSN-}c@79c?HHFz|LH}*o@roz+E1Q|Tn5c2jEr2rI;axTk6e^sYaz|-UZ zH-QO2^k{sz74=!!QiPFbu=Q;yG(reAP{& RfT{oh002ovPDHLkV1g$1iR}OY diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index de339175d..a02c51c69 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -5,30 +5,31 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; class BitcoinHardwareWalletService { - BitcoinHardwareWalletService(this.ledger, this.device); + BitcoinHardwareWalletService(this.ledgerConnection); - final Ledger ledger; - final LedgerDevice device; + final LedgerConnection ledgerConnection; - Future> getAvailableAccounts({int index = 0, int limit = 5}) async { - final bitcoinLedgerApp = BitcoinLedgerApp(ledger); + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final bitcoinLedgerApp = BitcoinLedgerApp(ledgerConnection); - final masterFp = await bitcoinLedgerApp.getMasterFingerprint(device); - print(masterFp); + final masterFp = await bitcoinLedgerApp.getMasterFingerprint(); final accounts = []; final indexRange = List.generate(limit, (i) => i + index); for (final i in indexRange) { final derivationPath = "m/84'/0'/$i'"; - final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath); + final xpub = + await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); Bip32Slip10Secp256k1 hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); - final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet); + final address = generateP2WPKHAddress( + hd: hd, index: 0, network: BitcoinNetwork.mainnet); accounts.add(HardwareAccountData( address: address, diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 30f04667a..908897845 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -5,13 +5,13 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.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_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; -import 'package:cw_bitcoin/psbt_transaction_builder.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -19,7 +19,7 @@ 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_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; part 'bitcoin_wallet.g.dart'; @@ -61,8 +61,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, - currency: - networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, + currency: networkParam == BitcoinNetwork.testnet + ? CryptoCurrency.tbtc + : CryptoCurrency.btc, alwaysScan: alwaysScan, ) { // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) @@ -80,11 +81,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mainHd: hd, sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: networkParam ?? network, - masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + masterHd: + seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = + this.isEnabledAutoGenerateSubaddress; }); } @@ -185,8 +189,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo ??= DerivationInfo(); // set the default if not present: - walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; - walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; + walletInfo.derivationInfo!.derivationPath ??= + snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationType ??= + snp?.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; final mnemonic = keysData.mnemonic; @@ -228,15 +234,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); } - Ledger? _ledger; - LedgerDevice? _ledgerDevice; + LedgerConnection? _ledgerConnection; BitcoinLedgerApp? _bitcoinLedgerApp; - void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) { - _ledger = setLedger; - _ledgerDevice = setLedgerDevice; - _bitcoinLedgerApp = - BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + @override + void setLedgerConnection(LedgerConnection connection) { + _ledgerConnection = connection; + _bitcoinLedgerApp = BitcoinLedgerApp(_ledgerConnection!, + derivationPath: walletInfo.derivationInfo!.derivationPath!); } @override @@ -251,12 +256,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, }) async { - final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!); + final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(); final psbtReadyInputs = []; for (final utxo in utxos) { - final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); - final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + final rawTx = + await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = + publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, @@ -268,10 +275,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { )); } - final psbt = - PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); + final psbt = PSBTTransactionBuild( + inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); - final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); + final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt); return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex)); } @@ -279,14 +286,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null - ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) + ? walletAddresses.allAddresses + .firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; final isChange = addressEntry?.isHidden == true ? 1 : 0; final accountPath = walletInfo.derivationInfo?.derivationPath; - final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; + final derivationPath = + accountPath != null ? "$accountPath/$isChange/$index" : null; - final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!, + final signature = await _bitcoinLedgerApp!.signMessage( message: ascii.encode(message), signDerivationPath: derivationPath); return base64Encode(signature); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 697719894..04a3cae36 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -15,6 +15,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4d127c248..4b80cb201 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -6,7 +6,6 @@ import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; @@ -26,6 +25,8 @@ import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -37,10 +38,10 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; @@ -51,9 +52,10 @@ part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -abstract class ElectrumWalletBase - extends WalletBase - with Store, WalletKeysFile { +abstract class ElectrumWalletBase extends WalletBase< + ElectrumBalance, + ElectrumTransactionHistory, + ElectrumTransactionInfo> with Store, WalletKeysFile { ElectrumWalletBase({ required String password, required WalletInfo walletInfo, @@ -69,8 +71,8 @@ abstract class ElectrumWalletBase ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, - }) : accountHD = - getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo), + }) : accountHD = getAccountHDWallet( + currency, network, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -105,8 +107,12 @@ abstract class ElectrumWalletBase sharedPrefs.complete(SharedPreferences.getInstance()); } - static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, - Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { + static Bip32Slip10Secp256k1 getAccountHDWallet( + CryptoCurrency? currency, + BasedUtxoNetwork network, + Uint8List? seedBytes, + String? xpub, + DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { throw Exception( "To create a Wallet you need either a seed or an xpub. This should not happen"); @@ -117,8 +123,9 @@ abstract class ElectrumWalletBase case CryptoCurrency.btc: case CryptoCurrency.ltc: case CryptoCurrency.tbtc: - return Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( - _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)) + return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)) + .derivePath(_hardenedDerivationPath( + derivationInfo?.derivationPath ?? electrum_path)) as Bip32Slip10Secp256k1; case CryptoCurrency.bch: return bitcoinCashHDWallet(seedBytes); @@ -127,15 +134,26 @@ abstract class ElectrumWalletBase } } - return Bip32Slip10Secp256k1.fromExtendedKey(xpub!); + return Bip32Slip10Secp256k1.fromExtendedKey( + xpub!, getKeyNetVersion(network)); } static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") + as Bip32Slip10Secp256k1; static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; + static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) { + switch (network) { + case LitecoinNetwork.mainnet: + return Bip44Conf.litecoinMainNet.altKeyNetVer; + default: + return null; + } + } + bool? alwaysScan; final Bip32Slip10Secp256k1 accountHD; @@ -634,8 +652,9 @@ abstract class ElectrumWalletBase ECPrivate? privkey; bool? isSilentPayment = false; - final hd = - utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; + final hd = utx.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd; if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; @@ -833,7 +852,7 @@ abstract class ElectrumWalletBase inputs: utxoDetails.availableInputs, outputs: updatedOutputs, ); - final address = RegexUtils.addressTypeFromStr(changeAddress, network); + final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); updatedOutputs.add(BitcoinOutput( address: address, value: BigInt.from(amountLeftForChangeAndFee), @@ -845,6 +864,14 @@ abstract class ElectrumWalletBase isChange: true, )); + // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets + final changeDerivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "/${changeAddress.isHidden ? "1" : "0"}" + "/${changeAddress.index}"; + utxoDetails.publicKeys[address.pubKeyHash()] = + PublicKeyWithDerivationPath('', changeDerivationPath); + // calcFee updates the silent payment outputs to calculate the tx size accounting // for taproot addresses, but if more inputs are needed to make up for fees, // the silent payment outputs need to be recalculated for the new inputs @@ -1206,6 +1233,9 @@ abstract class ElectrumWalletBase } } + void setLedgerConnection(ledger.LedgerConnection connection) => + throw UnimplementedError(); + Future buildHardwareWalletTransaction({ required List outputs, required BigInt fee, @@ -1563,7 +1593,9 @@ abstract class ElectrumWalletBase final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); final privkey = generateECPrivate( - hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + hd: addressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, index: addressRecord.index, network: network); @@ -1745,7 +1777,8 @@ abstract class ElectrumWalletBase if (height != null) { if (time == null && height > 0) { - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000) + .round(); } if (confirmations == null) { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 6c10dc615..2686d12cc 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -36,6 +36,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required this.mainHd, required this.sideHd, required this.network, + required this.isHardwareWallet, List? initialAddresses, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, @@ -44,6 +45,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, + }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = ObservableList.of(([]).toSet()), @@ -112,6 +114,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; + final bool isHardwareWallet; @observable SilentPaymentOwner? silentAddress; @@ -240,15 +243,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); - if (Platform.isAndroid || Platform.isIOS) { + if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { await _generateInitialAddresses(type: SegwitAddresType.mweb); } } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); - await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); - await _generateInitialAddresses(type: SegwitAddresType.p2tr); - await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + if (!isHardwareWallet) { + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await _generateInitialAddresses(type: SegwitAddresType.p2tr); + await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + } } updateAddressesByMatch(); @@ -267,7 +272,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { + Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -282,7 +287,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } updateChangeAddresses(); - final address = changeAddresses[currentChangeAddressIndex].address; + final address = changeAddresses[currentChangeAddressIndex]; currentChangeAddressIndex += 1; return address; } @@ -670,7 +675,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd; + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => !addr.isHidden && !addr.isUsed && addr.type == type; diff --git a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart new file mode 100644 index 000000000..62840933c --- /dev/null +++ b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/ledger_litecoin.dart'; + +class LitecoinHardwareWalletService { + LitecoinHardwareWalletService(this.ledgerConnection); + + final LedgerConnection ledgerConnection; + + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final litecoinLedgerApp = LitecoinLedgerApp(ledgerConnection); + + await litecoinLedgerApp.getVersion(); + + final accounts = []; + final indexRange = List.generate(limit, (i) => i + index); + final xpubVersion = Bip44Conf.litecoinMainNet.altKeyNetVer; + + for (final i in indexRange) { + final derivationPath = "m/84'/2'/$i'"; + final xpub = await litecoinLedgerApp.getXPubKey( + accountsDerivationPath: derivationPath, + xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16)); + final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion) + .childKey(Bip32KeyIndex(0)); + + final address = generateP2WPKHAddress( + hd: hd, index: 0, network: LitecoinNetwork.mainnet); + + accounts.add(HardwareAccountData( + address: address, + accountIndex: i, + derivationPath: derivationPath, + xpub: xpub, + )); + } + + return accounts; + } +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index afa8cc388..1fb39c878 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; + import 'package:convert/convert.dart' as convert; import 'dart:math'; import 'package:collection/collection.dart'; @@ -37,6 +39,8 @@ import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:grpc/grpc.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/ledger_litecoin.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_mweb/cw_mweb.dart'; @@ -50,12 +54,13 @@ class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; abstract class LitecoinWalletBase extends ElectrumWallet with Store { LitecoinWalletBase({ - required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, - required Uint8List seedBytes, required EncryptionFileUtils encryptionFileUtils, + Uint8List? seedBytes, + String? mnemonic, + String? xpub, String? passphrase, String? addressPageType, List? initialAddresses, @@ -68,6 +73,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }) : super( mnemonic: mnemonic, password: password, + xpub: xpub, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, network: LitecoinNetwork.mainnet, @@ -78,8 +84,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { currency: CryptoCurrency.ltc, alwaysScan: alwaysScan, ) { - mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; - mwebEnabled = alwaysScan ?? false; + if (seedBytes != null) { + mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( + "m/1000'") as Bip32Slip10Secp256k1; + mwebEnabled = alwaysScan ?? false; + } else { + mwebHd = null; + mwebEnabled = false; + } walletAddresses = LitecoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -91,6 +103,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { network: network, mwebHd: mwebHd, mwebEnabled: mwebEnabled, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; @@ -126,7 +139,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } }); } - late final Bip32Slip10Secp256k1 mwebHd; + late final Bip32Slip10Secp256k1? mwebHd; late final Box mwebUtxosBox; Timer? _syncTimer; Timer? _feeRatesTimer; @@ -138,8 +151,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @observable SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); - List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; - List get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; static Future create( {required String mnemonic, @@ -249,14 +262,15 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } return LitecoinWallet( - mnemonic: keysData.mnemonic!, + mnemonic: keysData.mnemonic, + xpub: keysData.xPub, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp?.addresses, initialMwebAddresses: snp?.mwebAddresses, initialBalance: snp?.balance, - seedBytes: seedBytes!, + seedBytes: seedBytes, passphrase: passphrase, encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, @@ -935,7 +949,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { tx.changeAddressOverride = - await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false); + (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: false)) + .address; return tx; } await waitForMwebAddresses(); @@ -974,8 +990,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool isPegIn = !hasMwebInput && hasMwebOutput; bool isRegular = !hasMwebInput && !hasMwebOutput; - tx.changeAddressOverride = await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: isPegIn || isRegular); + tx.changeAddressOverride = + (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular)) + .address; if (!hasMwebInput && !hasMwebOutput) { tx.isMweb = false; return tx; @@ -1215,4 +1233,64 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return false; } + + LedgerConnection? _ledgerConnection; + LitecoinLedgerApp? _litecoinLedgerApp; + + @override + void setLedgerConnection(LedgerConnection connection) { + _ledgerConnection = connection; + _litecoinLedgerApp = + LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + } + + @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 readyInputs = []; + for (final utxo in utxos) { + final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + + readyInputs.add(LedgerTransaction( + rawTx: rawTx, + outputIndex: utxo.utxo.vout, + ownerPublicKey: Uint8List.fromList(hex.decode(publicKeyAndDerivationPath.publicKey)), + ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, + // sequence: enableRBF ? 0x1 : 0xffffffff, + sequence: 0xffffffff, + )); + } + + String? changePath; + for (final output in outputs) { + final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()]; + if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath; + } + + + final rawHex = await _litecoinLedgerApp!.createTransaction( + inputs: readyInputs, + outputs: outputs + .map((e) => TransactionOutput.fromBigInt( + (e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) + .toList(), + changePath: changePath, + sigHashType: 0x01, + additionals: ["bech32"], + isSegWit: true, + useTrustedInputForSegwit: true + ); + + return BtcTransaction.fromRaw(rawHex); + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 6154a0ead..c55f5fc76 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -16,14 +16,17 @@ import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase + with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses + with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, required this.mwebHd, required this.mwebEnabled, super.initialAddresses, @@ -37,20 +40,20 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with print("initialized with ${mwebAddrs.length} mweb addresses"); } - final Bip32Slip10Secp256k1 mwebHd; + final Bip32Slip10Secp256k1? mwebHd; bool mwebEnabled; int mwebTopUpIndex = 1000; List mwebAddrs = []; bool generating = false; - List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; - + List get scanSecret => + mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendPubkey => - mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; + mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @override Future init() async { - await initMwebAddresses(); + if (!isHardwareWallet) await initMwebAddresses(); await super.init(); } @@ -94,6 +97,9 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with print("Done generating MWEB addresses len: ${mwebAddrs.length}"); // ensure mweb addresses are up to date: + // This is the Case if the Litecoin Wallet is a hardware Wallet + if (mwebHd == null) return; + if (mwebAddresses.length < mwebAddrs.length) { List addressRecords = mwebAddrs .asMap() @@ -143,7 +149,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override - Future getChangeAddress( + Future getChangeAddress( {List? inputs, List? outputs, bool isPegIn = false}) async { // use regular change address on peg in, otherwise use mweb for change address: @@ -187,7 +193,12 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with if (mwebEnabled) { await ensureMwebAddressUpToIndexExists(1); - return mwebAddrs[0]; + return BitcoinAddressRecord( + mwebAddrs[0], + index: 0, + type: SegwitAddresType.mweb, + network: network, + ); } return super.getChangeAddress(); diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index c659dd658..7cc266f5b 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_core/encryption_file_utils.dart'; @@ -20,7 +21,7 @@ class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, - BitcoinNewWalletCredentials> { + BitcoinRestoreWalletFromHardware> { LitecoinWalletService( this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); @@ -147,9 +148,23 @@ class LitecoinWalletService extends WalletService< } @override - Future restoreFromHardwareWallet(BitcoinNewWalletCredentials credentials) { - throw UnimplementedError( - "Restoring a Litecoin wallet from a hardware wallet is not yet supported!"); + Future restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials, + {bool? isTestnet}) async { + final network = isTestnet == true ? LitecoinNetwork.testnet : LitecoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + credentials.walletInfo?.derivationInfo?.derivationPath = + credentials.hwAccountData.derivationPath; + + final wallet = await LitecoinWallet( + password: credentials.password!, + xpub: credentials.hwAccountData.xpub, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + await wallet.save(); + await wallet.init(); + return wallet; } @override diff --git a/cw_bitcoin/lib/psbt_transaction_builder.dart b/cw_bitcoin/lib/psbt_transaction_builder.dart index d8d2c9fac..81efb792e 100644 --- a/cw_bitcoin/lib/psbt_transaction_builder.dart +++ b/cw_bitcoin/lib/psbt_transaction_builder.dart @@ -16,10 +16,6 @@ class PSBTTransactionBuild { for (var i = 0; i < inputs.length; i++) { final input = inputs[i]; - print(input.utxo.isP2tr()); - print(input.utxo.isSegwit()); - print(input.utxo.isP2shSegwit()); - psbt.setInputPreviousTxId(i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList())); psbt.setInputOutputIndex(i, input.utxo.vout); psbt.setInputSequence(i, enableRBF ? 0x1 : 0xffffffff); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 36d762ea1..5cba9b734 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -101,6 +101,14 @@ packages: url: "https://github.com/cake-tech/blockchain_utils" source: git version: "3.3.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "203a1924e818a9dd74af2b2c7a8f375ab8e5edf0e486bba8f90a0d8a17ed9fce" + url: "https://pub.dev" + source: hosted + version: "0.8.2" boolean_selector: dependency: transitive description: @@ -300,6 +308,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" encrypt: dependency: transitive description: @@ -361,19 +377,19 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1+1" - flutter_reactive_ble: - dependency: transitive - description: - name: flutter_reactive_ble - sha256: "247e2efa76de203d1ba11335c13754b5b9d0504b5423e5b0c93a600f016b24e0" - url: "https://pub.dev" - source: hosted - version: "5.3.1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" + url: "https://pub.dev" + source: hosted + version: "0.2.3" flutter_web_plugins: dependency: transitive description: flutter @@ -387,14 +403,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - functional_data: - dependency: transitive - description: - name: functional_data - sha256: "76d17dc707c40e552014f5a49c0afcc3f1e3f05e800cd6b7872940bfe41a5039" - url: "https://pub.dev" - source: hosted - version: "1.2.0" glob: dependency: transitive description: @@ -550,29 +558,37 @@ packages: ledger_bitcoin: dependency: "direct main" description: - path: "." + path: "packages/ledger-bitcoin" ref: HEAD - resolved-ref: f819d37e235e239c315e93856abbf5e5d3b71dab - url: "https://github.com/cake-tech/ledger-bitcoin" + resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git - version: "0.0.2" - ledger_flutter: + version: "0.0.3" + ledger_flutter_plus: dependency: "direct main" description: - path: "." - ref: cake-v3 - resolved-ref: "66469ff9dffe2417c70ae7287c9d76d2fe7157a4" - url: "https://github.com/cake-tech/ledger-flutter.git" - source: git - version: "1.0.2" - ledger_usb: - dependency: transitive - description: - name: ledger_usb - sha256: "52c92d03a4cffe06c82921c8e2f79f3cdad6e1cf78e1e9ca35444196ff8f14c2" + name: ledger_flutter_plus + sha256: ea3ed586e1697776dacf42ac979095f1ca3bd143bf007cbe5c78e09cb6943f42 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.5" + ledger_litecoin: + dependency: "direct main" + description: + path: "packages/ledger-litecoin" + ref: HEAD + resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" + source: git + version: "0.0.2" + ledger_usb_plus: + dependency: transitive + description: + name: ledger_usb_plus + sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" + url: "https://pub.dev" + source: hosted + version: "1.0.4" logging: dependency: transitive description: @@ -701,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -773,30 +797,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" - reactive_ble_mobile: - dependency: transitive - description: - name: reactive_ble_mobile - sha256: "9ec2b4c9c725e439950838d551579750060258fbccd5536d0543b4d07d225798" - url: "https://pub.dev" - source: hosted - version: "5.3.1" - reactive_ble_platform_interface: - dependency: transitive - description: - name: reactive_ble_platform_interface - sha256: "632c92401a2d69c9b94bd48f8fd47488a7013f3d1f9b291884350291a4a81813" - url: "https://pub.dev" - source: hosted - version: "5.3.1" rxdart: dependency: "direct main" description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" shared_preferences: dependency: "direct main" description: @@ -987,6 +995,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_ble: + dependency: transitive + description: + name: universal_ble + sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" + url: "https://pub.dev" + source: hosted + version: "0.12.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" unorm_dart: dependency: transitive description: @@ -1043,6 +1067,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 7e33d8260..9f1cee67d 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -24,16 +24,12 @@ dependencies: git: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data - rxdart: ^0.27.5 + rxdart: ^0.28.0 cryptography: ^2.0.5 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v2 - ledger_flutter: ^1.0.1 - ledger_bitcoin: - git: - url: https://github.com/cake-tech/ledger-bitcoin cw_mweb: path: ../cw_mweb grpc: ^3.2.4 @@ -44,6 +40,15 @@ dependencies: bech32: git: url: https://github.com/cake-tech/bech32.git + ledger_flutter_plus: ^1.4.1 + ledger_bitcoin: + git: + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-bitcoin + ledger_litecoin: + git: + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-litecoin dev_dependencies: flutter_test: @@ -54,10 +59,6 @@ dev_dependencies: hive_generator: ^1.1.3 dependency_overrides: - ledger_flutter: - git: - url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-v3 watcher: ^1.1.0 protobuf: ^3.1.0 bitcoin_base: diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 825c80d4a..d55914dcd 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -58,6 +58,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, initialAddressPageType: addressPageType, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index 7342dc7f5..fe0ebc828 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -15,6 +15,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 99fd5b1f0..9a3069552 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -8,6 +8,7 @@ enum DeviceConnectionType { [bool isIOS = false]) { switch (walletType) { case WalletType.bitcoin: + case WalletType.litecoin: case WalletType.ethereum: case WalletType.polygon: if (isIOS) return [DeviceConnectionType.ble]; diff --git a/cw_evm/lib/evm_chain_hardware_wallet_service.dart b/cw_evm/lib/evm_chain_hardware_wallet_service.dart index 6f0d11f2e..d8f67c641 100644 --- a/cw_evm/lib/evm_chain_hardware_wallet_service.dart +++ b/cw_evm/lib/evm_chain_hardware_wallet_service.dart @@ -2,26 +2,26 @@ import 'dart:async'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_ethereum/ledger_ethereum.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; class EVMChainHardwareWalletService { - EVMChainHardwareWalletService(this.ledger, this.device); + EVMChainHardwareWalletService(this.ledgerConnection); - final Ledger ledger; - final LedgerDevice device; + final LedgerConnection ledgerConnection; - Future> getAvailableAccounts({int index = 0, int limit = 5}) async { - final ethereumLedgerApp = EthereumLedgerApp(ledger); + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final ethereumLedgerApp = EthereumLedgerApp(ledgerConnection); - final version = await ethereumLedgerApp.getVersion(device); + await ethereumLedgerApp.getVersion(); final accounts = []; final indexRange = List.generate(limit, (i) => i + index); for (final i in indexRange) { final derivationPath = "m/44'/60'/$i'/0/0"; - final address = - await ethereumLedgerApp.getAccounts(device, accountsDerivationPath: derivationPath); + final address = await ethereumLedgerApp.getAccounts( + accountsDerivationPath: derivationPath); accounts.add(HardwareAccountData( address: address.first, diff --git a/cw_evm/lib/evm_chain_transaction_credentials.dart b/cw_evm/lib/evm_chain_transaction_credentials.dart index 02927cb4d..5b5bdf170 100644 --- a/cw_evm/lib/evm_chain_transaction_credentials.dart +++ b/cw_evm/lib/evm_chain_transaction_credentials.dart @@ -1,7 +1,6 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; class EVMChainTransactionCredentials { EVMChainTransactionCredentials( diff --git a/cw_evm/lib/evm_ledger_credentials.dart b/cw_evm/lib/evm_ledger_credentials.dart index 0d8de1736..a0b7788dc 100644 --- a/cw_evm/lib/evm_ledger_credentials.dart +++ b/cw_evm/lib/evm_ledger_credentials.dart @@ -1,17 +1,16 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:cw_core/hardware/device_not_connected_exception.dart'; +import 'package:cw_core/hardware/device_not_connected_exception.dart' + as exception; import 'package:ledger_ethereum/ledger_ethereum.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; class EvmLedgerCredentials extends CredentialsWithKnownAddress { final String _address; - Ledger? ledger; - LedgerDevice? ledgerDevice; EthereumLedgerApp? ethereumLedgerApp; EvmLedgerCredentials(this._address); @@ -19,25 +18,25 @@ class EvmLedgerCredentials extends CredentialsWithKnownAddress { @override EthereumAddress get address => EthereumAddress.fromHex(_address); - void setLedger(Ledger setLedger, [LedgerDevice? setLedgerDevice, String? derivationPath]) { - ledger = setLedger; - ledgerDevice = setLedgerDevice; - ethereumLedgerApp = - EthereumLedgerApp(ledger!, derivationPath: derivationPath ?? "m/44'/60'/0'/0/0"); + void setLedgerConnection(LedgerConnection connection, + [String? derivationPath]) { + ethereumLedgerApp = EthereumLedgerApp(connection, + derivationPath: derivationPath ?? "m/44'/60'/0'/0/0"); } @override - MsgSignature signToEcSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) => - throw UnimplementedError("EvmLedgerCredentials.signToEcSignature"); + MsgSignature signToEcSignature(Uint8List payload, + {int? chainId, bool isEIP1559 = false}) => + throw UnimplementedError("EvmLedgerCredentials.signToEcSignature"); @override Future signToSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) async { - if (ledgerDevice == null && ledger?.devices.isNotEmpty != true) { - throw DeviceNotConnectedException(); + if (ethereumLedgerApp == null) { + throw exception.DeviceNotConnectedException(); } - final sig = await ethereumLedgerApp!.signTransaction(device, payload); + final sig = await ethereumLedgerApp!.signTransaction(payload); final v = sig[0].toInt(); final r = bytesToHex(sig.sublist(1, 1 + 32)); @@ -65,14 +64,16 @@ class EvmLedgerCredentials extends CredentialsWithKnownAddress { chainIdV = chainId != null ? (parity + (chainId * 2 + 35)) : parity; } - return MsgSignature(BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), chainIdV); + return MsgSignature( + BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), chainIdV); } @override - Future signPersonalMessage(Uint8List payload, {int? chainId}) async { - if (isNotConnected) throw DeviceNotConnectedException(); + Future signPersonalMessage(Uint8List payload, + {int? chainId}) async { + if (isNotConnected) throw exception.DeviceNotConnectedException(); - final sig = await ethereumLedgerApp!.signMessage(device, payload); + final sig = await ethereumLedgerApp!.signMessage(payload); final r = sig.sublist(1, 1 + 32); final s = sig.sublist(1 + 32, 1 + 32 + 32); @@ -84,20 +85,22 @@ class EvmLedgerCredentials extends CredentialsWithKnownAddress { @override Uint8List signPersonalMessageToUint8List(Uint8List payload, {int? chainId}) => - throw UnimplementedError("EvmLedgerCredentials.signPersonalMessageToUint8List"); + throw UnimplementedError( + "EvmLedgerCredentials.signPersonalMessageToUint8List"); - Future provideERC20Info(String erc20ContractAddress, int chainId) async { - if (isNotConnected) throw DeviceNotConnectedException(); + Future provideERC20Info( + String erc20ContractAddress, int chainId) async { + if (isNotConnected) throw exception.DeviceNotConnectedException(); try { - await ethereumLedgerApp!.getAndProvideERC20TokenInformation(device, + await ethereumLedgerApp!.getAndProvideERC20TokenInformation( erc20ContractAddress: erc20ContractAddress, chainId: chainId); - } on LedgerException catch (e) { - if (e.errorCode != -28672) rethrow; + } catch (e) { + print(e); + rethrow; + // if (e.errorCode != -28672) rethrow; } } - bool get isNotConnected => (ledgerDevice ?? ledger?.devices.firstOrNull) == null; - - LedgerDevice get device => ledgerDevice ?? ledger!.devices.first; + bool get isNotConnected => ethereumLedgerApp == null || ethereumLedgerApp!.connection.isDisconnected; } diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index 3e12834b1..326ff4dc9 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -25,20 +25,17 @@ dependencies: mobx: ^2.0.7+4 cw_core: path: ../cw_core - ledger_flutter: ^1.0.1 + ledger_flutter_plus: ^1.4.1 ledger_ethereum: git: - url: https://github.com/cake-tech/ledger-ethereum.git + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-ethereum dependency_overrides: web3dart: git: url: https://github.com/cake-tech/web3dart.git ref: cake - ledger_flutter: - git: - url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-v3 watcher: ^1.1.0 dev_dependencies: diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 365d86be5..60364c289 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -487,18 +487,30 @@ class CWBitcoin extends Bitcoin { } @override - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { - (wallet as BitcoinWallet).setLedger(ledger, device); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection) { + (wallet as ElectrumWallet).setLedgerConnection(connection); } @override - Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, + Future> getHardwareWalletBitcoinAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}) async { - final hardwareWalletService = BitcoinHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + final hardwareWalletService = BitcoinHardwareWalletService(ledgerVM.connection); try { return hardwareWalletService.getAvailableAccounts(index: index, limit: limit); - } on LedgerException catch (err) { - print(err.message); + } catch (err) { + print(err); + throw err; + } + } + + @override + Future> getHardwareWalletLitecoinAccounts(LedgerViewModel ledgerVM, + {int index = 0, int limit = 5}) async { + final hardwareWalletService = LitecoinHardwareWalletService(ledgerVM.connection); + try { + return hardwareWalletService.getAvailableAccounts(index: index, limit: limit); + } catch (err) { + print(err); throw err; } } diff --git a/lib/entities/hardware_wallet/hardware_wallet_device.dart b/lib/entities/hardware_wallet/hardware_wallet_device.dart new file mode 100644 index 000000000..d3acc5d32 --- /dev/null +++ b/lib/entities/hardware_wallet/hardware_wallet_device.dart @@ -0,0 +1,65 @@ +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; + +class HardwareWalletDevice { + final String name; + final HardwareWalletDeviceType type; + final HardwareWalletConnectionType connectionType; + + const HardwareWalletDevice({ + required this.name, + required this.type, + required this.connectionType, + }); + + factory HardwareWalletDevice.fromLedgerDevice(ledger.LedgerDevice device) => + HardwareWalletDevice( + name: device.name, + type: device.deviceInfo.toGeneric(), + connectionType: device.connectionType.toGeneric(), + ); +} + +enum HardwareWalletDeviceType { + ledgerBlue, + ledgerNanoS, + ledgerNanoX, + ledgerNanoSPlus, + ledgerStax, + ledgerFlex; +} + +enum HardwareWalletConnectionType { + usb, + ble, + nfc; +} + +extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType { + HardwareWalletDeviceType toGeneric() { + switch (this) { + case ledger.LedgerDeviceType.blue: + return HardwareWalletDeviceType.ledgerBlue; + case ledger.LedgerDeviceType.nanoS: + return HardwareWalletDeviceType.ledgerNanoS; + case ledger.LedgerDeviceType.nanoSP: + return HardwareWalletDeviceType.ledgerNanoSPlus; + case ledger.LedgerDeviceType.nanoX: + return HardwareWalletDeviceType.ledgerNanoX; + case ledger.LedgerDeviceType.stax: + return HardwareWalletDeviceType.ledgerStax; + case ledger.LedgerDeviceType.flex: + return HardwareWalletDeviceType.ledgerFlex; + } + } +} + +extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType { + HardwareWalletConnectionType toGeneric() { + switch (this) { + case ledger.ConnectionType.usb: + return HardwareWalletConnectionType.usb; + case ledger.ConnectionType.ble: + return HardwareWalletConnectionType.ble; + } + } +} diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index e2c3d1358..7a06a1679 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -191,21 +191,21 @@ class CWEthereum extends Ethereum { String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials).setLedger( - ledger, - device.connectionType == ConnectionType.usb ? device : null, - wallet.walletInfo.derivationInfo?.derivationPath); + void setLedgerConnection( + WalletBase wallet, ledger.LedgerConnection connection) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials) + .setLedgerConnection( + connection, wallet.walletInfo.derivationInfo?.derivationPath); } @override Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}) async { - final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.connection); try { return await hardwareWalletService.getAvailableAccounts(index: index, limit: limit); - } on LedgerException catch (err) { - print(err.message); + } catch (err) { + print(err); throw err; } } diff --git a/lib/main.dart b/lib/main.dart index 2b5bef06c..29b216b22 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/app_scroll_behavior.dart'; import 'package:cake_wallet/buy/order.dart'; @@ -43,6 +44,7 @@ import 'package:hive/hive.dart'; import 'package:cw_core/root_dir.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/window_size.dart'; +import 'package:logging/logging.dart'; final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); @@ -68,8 +70,18 @@ Future runAppWithZone({Key? topLevelKey}) async { }; await initializeAppAtRoot(); - runApp(App(key: topLevelKey)); + if (kDebugMode) { + final appDocDir = await getAppDir(); + final ledgerFile = File('${appDocDir.path}/ledger_log.txt'); + if (!ledgerFile.existsSync()) ledgerFile.createSync(); + Logger.root.onRecord.listen((event) async { + final content = ledgerFile.readAsStringSync(); + ledgerFile.writeAsStringSync("$content\n${event.message}"); + }); + } + + runApp(App(key: topLevelKey)); isAppRunning = true; }, (error, stackTrace) async { if (!isAppRunning) { diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 299c8278a..74b4026eb 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -190,20 +190,21 @@ class CWPolygon extends Polygon { String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials).setLedger( - ledger, - device.connectionType == ConnectionType.usb ? device : null, - wallet.walletInfo.derivationInfo?.derivationPath); + void setLedgerConnection( + WalletBase wallet, ledger.LedgerConnection connection) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials) + .setLedgerConnection( + connection, wallet.walletInfo.derivationInfo?.derivationPath); } @override Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}) async { - final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.connection); try { return await hardwareWalletService.getAvailableAccounts(index: index, limit: limit); - } on LedgerException catch (err) { + } catch (err) { + print(err); throw err; } } diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index a482b1c41..9e331e818 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -3,15 +3,14 @@ import 'dart:io'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/connect_device/debug_device_page.dart'; import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; -import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; typedef OnConnectDevice = void Function(BuildContext, LedgerViewModel); @@ -19,7 +18,8 @@ class ConnectDevicePageParams { final WalletType walletType; final OnConnectDevice onConnectDevice; - ConnectDevicePageParams({required this.walletType, required this.onConnectDevice}); + ConnectDevicePageParams( + {required this.walletType, required this.onConnectDevice}); } class ConnectDevicePage extends BasePage { @@ -35,7 +35,8 @@ class ConnectDevicePage extends BasePage { String get title => S.current.restore_title_from_hardware_wallet; @override - Widget body(BuildContext context) => ConnectDevicePageBody(walletType, onConnectDevice, ledgerVM); + Widget body(BuildContext context) => + ConnectDevicePageBody(walletType, onConnectDevice, ledgerVM); } class ConnectDevicePageBody extends StatefulWidget { @@ -43,47 +44,35 @@ class ConnectDevicePageBody extends StatefulWidget { final OnConnectDevice onConnectDevice; final LedgerViewModel ledgerVM; - const ConnectDevicePageBody(this.walletType, this.onConnectDevice, this.ledgerVM); + const ConnectDevicePageBody( + this.walletType, this.onConnectDevice, this.ledgerVM); @override ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState(); } class ConnectDevicePageBodyState extends State { - final imageLedger = 'assets/images/ledger_nano.png'; - - final ledger = Ledger( - options: LedgerOptions( - scanMode: ScanMode.balanced, - maxScanDuration: const Duration(minutes: 5), - ), - onPermissionRequest: (_) async { - Map statuses = await [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.bluetoothAdvertise, - ].request(); - - return statuses.values.where((status) => status.isDenied).isEmpty; - }, - ); - - var bleIsEnabled = true; var bleDevices = []; var usbDevices = []; late Timer? _usbRefreshTimer = null; late Timer? _bleRefreshTimer = null; + late Timer? _bleStateTimer = null; late StreamSubscription? _bleRefresh = null; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - _bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); + _bleStateTimer = Timer.periodic( + Duration(seconds: 1), (_) => widget.ledgerVM.updateBleState()); + + _bleRefreshTimer = + Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); if (Platform.isAndroid) { - _usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); + _usbRefreshTimer = + Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); } }); } @@ -91,35 +80,59 @@ class ConnectDevicePageBodyState extends State { @override void dispose() { _bleRefreshTimer?.cancel(); + _bleStateTimer?.cancel(); _usbRefreshTimer?.cancel(); _bleRefresh?.cancel(); super.dispose(); } Future _refreshUsbDevices() async { - final dev = await ledger.listUsbDevices(); + final dev = await widget.ledgerVM.ledgerPlusUSB.devices; if (usbDevices.length != dev.length) setState(() => usbDevices = dev); + // _usbRefresh = widget.ledgerVM + // .scanForUsbDevices() + // .listen((device) => setState(() => usbDevices.add(device))) + // ..onError((e) { + // throw e.toString(); + // }); + // Keep polling until the lfp lib gets updated + // _usbRefreshTimer?.cancel(); + // _usbRefreshTimer = null; } Future _refreshBleDevices() async { try { - _bleRefresh = ledger.scan().listen((device) => setState(() => bleDevices.add(device))) + _bleRefresh = widget.ledgerVM + .scanForBleDevices() + .listen((device) => setState(() => bleDevices.add(device))) ..onError((e) { throw e.toString(); }); - setState(() => bleIsEnabled = true); _bleRefreshTimer?.cancel(); _bleRefreshTimer = null; } catch (e) { - setState(() => bleIsEnabled = false); + print(e); } } Future _connectToDevice(LedgerDevice device) async { - await widget.ledgerVM.connectLedger(device); + await widget.ledgerVM.connectLedger(device, widget.walletType); widget.onConnectDevice(context, widget.ledgerVM); } + String _getDeviceTileLeading(LedgerDeviceType deviceInfo) { + switch (deviceInfo) { + case LedgerDeviceType.nanoX: + return 'assets/images/hardware_wallet/ledger_nano_x.png'; + case LedgerDeviceType.stax: + return 'assets/images/hardware_wallet/ledger_stax.png'; + case LedgerDeviceType.flex: + return 'assets/images/hardware_wallet/ledger_flex.png'; + default: + return 'assets/images/hardware_wallet/ledger_nano_x.png'; + } + } + @override Widget build(BuildContext context) { return Center( @@ -139,7 +152,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor), + color: Theme.of(context) + .extension()! + .titleColor), textAlign: TextAlign.center, ), ), @@ -152,18 +167,25 @@ class ConnectDevicePageBodyState extends State { // title: "Debug Ledger", // leading: imageLedger, // ), - if (!bleIsEnabled) - Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Text( - S.of(context).ledger_please_enable_bluetooth, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor), - textAlign: TextAlign.center, + Observer( + builder: (_) => Offstage( + offstage: widget.ledgerVM.bleIsEnabled, + child: Padding( + padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + child: Text( + S.of(context).ledger_please_enable_bluetooth, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .titleColor), + textAlign: TextAlign.center, + ), ), ), + ), + if (bleDevices.length > 0) ...[ Padding( padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), @@ -174,7 +196,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.titleColor, + color: Theme.of(context) + .extension()! + .titleColor, ), ), ), @@ -186,7 +210,7 @@ class ConnectDevicePageBodyState extends State { child: DeviceTile( onPressed: () => _connectToDevice(device), title: device.name, - leading: imageLedger, + leading: _getDeviceTileLeading(device.deviceInfo), connectionType: device.connectionType, ), ), @@ -203,7 +227,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.titleColor, + color: Theme.of(context) + .extension()! + .titleColor, ), ), ), @@ -215,7 +241,7 @@ class ConnectDevicePageBodyState extends State { child: DeviceTile( onPressed: () => _connectToDevice(device), title: device.name, - leading: imageLedger, + leading: _getDeviceTileLeading(device.deviceInfo), connectionType: device.connectionType, ), ), diff --git a/lib/src/screens/connect_device/debug_device_page.dart b/lib/src/screens/connect_device/debug_device_page.dart index f5a9ef2a4..bed9d59a7 100644 --- a/lib/src/screens/connect_device/debug_device_page.dart +++ b/lib/src/screens/connect_device/debug_device_page.dart @@ -1,15 +1,15 @@ -// import 'dart:convert'; +// import 'dart:typed_data'; // +// import 'package:basic_utils/basic_utils.dart'; +// import 'package:bitcoin_base/bitcoin_base.dart'; // import 'package:cake_wallet/src/screens/base_page.dart'; // import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; // import 'package:cake_wallet/src/widgets/primary_button.dart'; // import 'package:cake_wallet/utils/responsive_layout_util.dart'; -// import 'package:convert/convert.dart'; // import 'package:flutter/material.dart'; -// import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -// import 'package:ledger_flutter/ledger_flutter.dart'; +// import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +// import 'package:ledger_litecoin/ledger_litecoin.dart'; // import 'package:permission_handler/permission_handler.dart'; -// import 'package:polyseed/polyseed.dart'; // // class DebugDevicePage extends BasePage { // @override @@ -50,7 +50,9 @@ // }, // ); // -// late BitcoinLedgerApp btc; +// // late BitcoinLedgerApp btc; +// late LitecoinLedgerApp ltc; +// // var devices = []; // var status = ""; // var counter = 0; @@ -59,7 +61,8 @@ // @override // void initState() { // super.initState(); -// btc = BitcoinLedgerApp(ledger); +// // btc = BitcoinLedgerApp(ledger); +// ltc = LitecoinLedgerApp(ledger); // } // // @override @@ -81,7 +84,7 @@ // // @override // Widget build(BuildContext context) { -// final imageLedger = 'assets/images/ledger_nano.png'; +// final imageLedger = 'assets/images/hardware_wallet/ledger_nano_x.png'; // // return Center( // child: Container( @@ -99,40 +102,25 @@ // DebugButton( // title: "Get Version", // method: "Version", -// func: () async => await btc.getVersion(selectedDevice!), -// ), -// DebugButton( -// title: "Get Master Fingerprint", -// method: "Master Fingerprint", -// func: () async => hex.encode(await btc.getMasterFingerprint(selectedDevice!)), -// ), -// DebugButton( -// title: "Get XPub", -// method: "XPub", -// func: () async => await btc.getXPubKey(selectedDevice!, derivationPath: "m/84'/0'/$counter'"), +// // func: () async => await btc.getVersion(selectedDevice!), +// func: () async => await ltc.getVersion(selectedDevice!), // ), // DebugButton( // title: "Get Wallet Address", // method: "Wallet Address", // func: () async { // setState(() => counter++); -// final derivationPath = "m/84'/0'/$counter'/0/0"; -// return await btc.getAccounts(selectedDevice!, accountsDerivationPath: derivationPath); +// final derivationPath = "m/84'/2'/0'/0/0"; +// return await ltc.getAccounts(selectedDevice!, +// accountsDerivationPath: derivationPath); +// // return await btc.getAccounts(selectedDevice!, accountsDerivationPath: derivationPath); // // return await ethereum!.getHardwareWalletAccounts(selectedDevice!); -// }, +// }, // ), // DebugButton( // title: "Send Money", -// method: "Sig", -// func: () async { -// final psbt = PsbtV2(); -// final psbtBuf = base64.decode( -// "cHNidP8BAgQCAAAAAQQBAQEFAQIAAQ4gTW6k/cwKKu1u7m9oKr5ob7VcAC0IPkfaDitRi/FkD7sBDwQAAAAAARAE/////wEA/ekBAQAAAAABA9AYVQLI722H0osKMa/4dvMucrnKV1Myxtlp0l0BoOBDAQAAAAD/////ku6r2ABaHt9N26f/P4eMljX8t1f4lBcFfEwuNm/uXYoBAAAAAP////+YeAl8arEGKOcyrWJAYwSboyCstkhHN8zn7/vy7pkYTAEAAAAA/////wHlHgAAAAAAABYAFKdq0umSucBGVkl2MpT6Hgo/0a/xAkcwRAIgMkiJmNFbEi2I3CQYOwyV/JepCnFQRvj4xghkySpFcJMCIGAypkkWltfj+ucvqUIu27tusDAIAAB+rBhX/GV7hPlEASEDyLmWyTLjLfC9kn8pnW42jW5N6EJo5fObjWWEyfLDu9UCSDBFAiEAg9crVtwBPF+sWk+Th6pLwzDjJGItwsUCvoBPtmMTEb4CIDGuM7WOguV0TP21oidF3bSUZlEAjUHWfWzxLKw+3LofASEDfN16xKb70UZSeQyX5Tlh8iRq7np5Nlz9GYdcSU50sKwCSDBFAiEAvotOblaEiBptRWkvb6bj2MGyRjTphKLBLiHYmrRMTCgCIEKJH+z65uPSSz1NIb0d/u3bU9l0xcWk0idEsXjB+BIiASEDrAEiEtrSNKxbh6F/KPaCTafF2LVjCzb75WB+x4xSuoQAAAAAAQEf5R4AAAAAAAAWABSnatLpkrnARlZJdjKU+h4KP9Gv8SIGA3xMuxmPsBAm9aMEUBs3N46DB+Kdts3bZR/Wxt+uM0H4GKtN6bpUAACAAAAAgAAAAIAAAAAAAAAAAAABBBTk7bEOxYcdXDi1eeWraYDufm6eJgEDCOgDAAAAAAAAAAEEFDX3g/pnDXIfsRw8shK42NZn+SdpAQMIiBMAAAAAAAAiAgN8TLsZj7AQJvWjBFAbNzeOgwfinbbN22Uf1sbfrjNB+BirTem6VAAAgAAAAIAAAACAAAAAAAAAAAAA" -// ); -// psbt.deserialize(psbtBuf); -// final result = await btc.signPsbt(selectedDevice!, psbt: psbt); -// return result.toHexString(); -// }, +// method: "Raw Tx", +// func: sendMoney // ), // Padding( // padding: EdgeInsets.only(top: 20), @@ -147,18 +135,18 @@ // ...devices // .map( // (device) => Padding( -// padding: EdgeInsets.only(bottom: 20), -// child: DeviceTile( -// onPressed: () { -// setState(() => selectedDevice = device); -// ledger.connect(device); -// }, -// title: device.name, -// leading: imageLedger, -// connectionType: device.connectionType, -// ), -// ), -// ) +// padding: EdgeInsets.only(bottom: 20), +// child: DeviceTile( +// onPressed: () { +// setState(() => selectedDevice = device); +// ledger.connect(device); +// }, +// title: device.name, +// leading: imageLedger, +// connectionType: device.connectionType, +// ), +// ), +// ) // .toList(), // PrimaryButton( // text: "Refresh BLE", @@ -188,6 +176,42 @@ // ); // } // +// Future sendMoney() async { +// final readyInputs = [ +// LedgerTransaction( +// rawTx: "010000000001018c055c85c3724c98842d27712771dd0de139711f5940bba2df4615c5522184740000000017160014faf7f6dfb4e70798b92c93f33b4c51024491829df0ffffff022b05c70000000000160014f489f947fd13a1fb44ac168427081d3f30b6ce0cde9dd82e0000000017a914d5eca376cb49d65031220ff9093b7d407073ed0d8702483045022100f648c9f6a9b8f35b6ec29bbfae312c95ed3d56ce6a3f177d994efe90562ec4bd02205b82ce2c94bc0c9d152c3afc668b200bd82f48d6a14e83c66ba0f154cd5f69190121038f1dca119420d4aa7ad04af1c0d65304723789cccc56d335b18692390437f35900000000", +// outputIndex: 0, +// ownerPublicKey: +// HexUtils.decode("03b2e67958ed3356e329e05cf94c3bee6b20c17175ac3b2a1278e073bf44f5d6ec"), +// ownerDerivationPath: "m/84'/2'/0'/0/0", +// sequence: 0xffffffff, +// ) +// ]; +// +// final outputs = [ +// BitcoinOutput( +// address: P2wpkhAddress.fromAddress( +// address: "ltc1qn0g5e36xaj07lqj6w9xn52ng07hud42g3jf5ps", +// network: LitecoinNetwork.mainnet), +// value: BigInt.from(1000000)), +// BitcoinOutput( +// address: P2wpkhAddress.fromAddress( +// address: "ltc1qrx29qz4ghu4j0xk37ptgk7034cwpmjyxhrcnk9", +// network: LitecoinNetwork.mainnet), +// value: BigInt.from(12042705)), +// ]; +// return await ltc.createTransaction(selectedDevice!, +// inputs: readyInputs, +// outputs: outputs +// .map((e) => TransactionOutput.fromBigInt( +// e.value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) +// .toList(), +// sigHashType: 0x01, +// additionals: ["bech32"], +// isSegWit: true, +// useTrustedInputForSegwit: true); +// } +// // Widget DebugButton( // {required String title, required String method, required Future Function() func}) { // return Padding( diff --git a/lib/src/screens/connect_device/widgets/device_tile.dart b/lib/src/screens/connect_device/widgets/device_tile.dart index 8367d1606..58f65c5de 100644 --- a/lib/src/screens/connect_device/widgets/device_tile.dart +++ b/lib/src/screens/connect_device/widgets/device_tile.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:flutter/material.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; class DeviceTile extends StatelessWidget { const DeviceTile({ diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 478f1a3c3..028b3cfcf 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -188,7 +188,7 @@ class CryptoBalanceWidget extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Image.asset( - 'assets/images/ledger_nano.png', + 'assets/images/hardware_wallet/ledger_nano_x.png', width: 24, color: Theme.of(context) .extension()! diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index d671230c4..57f5ec727 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -67,7 +67,7 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { final mainImageColor = Theme.of(context).extension()!.pageTitleTextColor; final brightImageColor = Theme.of(context).extension()!.textColor; final imageColor = widget.themeType == ThemeType.bright ? brightImageColor : mainImageColor; - final imageLedger = Image.asset('assets/images/ledger_nano.png', width: 40, color: imageColor); + final imageLedger = Image.asset('assets/images/hardware_wallet/ledger_nano_x.png', width: 40, color: imageColor); final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor); final imageBackup = Image.asset('assets/images/backup.png', color: imageColor); @@ -186,4 +186,4 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { } } } -} \ No newline at end of file +} diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index d881d0341..bce82312d 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -395,16 +395,19 @@ class SendPage extends BasePage { if (sendViewModel.wallet.isHardwareWallet) { if (!sendViewModel.ledgerViewModel!.isConnected) { - await Navigator.of(context).pushNamed(Routes.connectDevices, + await Navigator.of(context).pushNamed( + Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: sendViewModel.walletType, onConnectDevice: (BuildContext context, _) { - sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet); + sendViewModel.ledgerViewModel! + .setLedger(sendViewModel.wallet); Navigator.of(context).pop(); }, )); } else { - sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet); + sendViewModel.ledgerViewModel! + .setLedger(sendViewModel.wallet); } } @@ -509,7 +512,7 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); - + if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) { newContactAddress = null; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 26bc5ceb9..53a8277ab 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -435,7 +435,7 @@ abstract class DashboardViewModelBase with Store { } @computed - bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid); + bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid) && !wallet.isHardwareWallet; @computed bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled; diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index f05b1c805..19b190fe3 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; @@ -9,11 +10,19 @@ import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; + +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk; +import 'package:mobx/mobx.dart'; import 'package:permission_handler/permission_handler.dart'; -class LedgerViewModel { - late final Ledger ledger; +part 'ledger_view_model.g.dart'; + +class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel; + +abstract class LedgerViewModelBase with Store { + // late final Ledger ledger; + late final sdk.LedgerInterface ledgerPlusBLE; + late final sdk.LedgerInterface ledgerPlusUSB; bool get _doesSupportHardwareWallets { if (!DeviceInfo.instance.isMobile) { @@ -21,53 +30,97 @@ class LedgerViewModel { } if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + return DeviceConnectionType.supportedConnectionTypes( + WalletType.monero, Platform.isIOS) .isNotEmpty; } return true; } - LedgerViewModel() { + LedgerViewModelBase() { if (_doesSupportHardwareWallets) { - ledger = Ledger( - options: LedgerOptions( - scanMode: ScanMode.balanced, - maxScanDuration: const Duration(minutes: 5), - ), - onPermissionRequest: (_) async { - Map statuses = await [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.bluetoothAdvertise, - ].request(); + reaction((_) => bleIsEnabled, (_) { + if (bleIsEnabled) _initBLE(); + }); + updateBleState(); - return statuses.values.where((status) => status.isDenied).isEmpty; - }, - ); + if (!Platform.isIOS) { + ledgerPlusUSB = sdk.LedgerInterface.usb(); + } } } - Future connectLedger(LedgerDevice device) async { - await ledger.connect(device); + @observable + bool bleIsEnabled = false; - if (device.connectionType == ConnectionType.usb) _device = device; + bool _bleIsInitialized = false; + Future _initBLE() async { + if (bleIsEnabled && !_bleIsInitialized) { + ledgerPlusBLE = sdk.LedgerInterface.ble(onPermissionRequest: (_) async { + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.bluetoothAdvertise, + ].request(); + + return statuses.values.where((status) => status.isDenied).isEmpty; + }); + _bleIsInitialized = true; + } } - LedgerDevice? _device; + Future updateBleState() async { + final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState(); - bool get isConnected => ledger.devices.isNotEmpty || _device != null; + final newState = bleState == sdk.AvailabilityState.poweredOn; - LedgerDevice get device => _device ?? ledger.devices.first; + if (newState != bleIsEnabled) bleIsEnabled = newState; + } + + Stream scanForBleDevices() => ledgerPlusBLE.scan(); + + Stream scanForUsbDevices() => ledgerPlusUSB.scan(); + + Future connectLedger(sdk.LedgerDevice device, WalletType type) async { + if (isConnected) { + try { + await _connection!.disconnect(); + } catch (_) {} + } + final ledger = device.connectionType == sdk.ConnectionType.ble + ? ledgerPlusBLE + : ledgerPlusUSB; + + if (_connectionChangeListener == null) { + _connectionChangeListener = ledger.deviceStateChanges.listen((event) { + print('Ledger Device State Changed: $event'); + if (event == sdk.BleConnectionState.disconnected) { + _connection = null; + _connectionChangeListener?.cancel(); + } + }); + } + + _connection = await ledger.connect(device); + } + + StreamSubscription? _connectionChangeListener; + sdk.LedgerConnection? _connection; + + bool get isConnected => _connection != null && !(_connection!.isDisconnected); + + sdk.LedgerConnection get connection => _connection!; void setLedger(WalletBase wallet) { switch (wallet.type) { case WalletType.bitcoin: - return bitcoin!.setLedger(wallet, ledger, device); + case WalletType.litecoin: + return bitcoin!.setLedgerConnection(wallet, connection); case WalletType.ethereum: - return ethereum!.setLedger(wallet, ledger, device); + return ethereum!.setLedgerConnection(wallet, connection); case WalletType.polygon: - return polygon!.setLedger(wallet, ledger, device); + return polygon!.setLedgerConnection(wallet, connection); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 4064a1258..24e83f364 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -24,7 +24,6 @@ import 'package:cw_core/unspent_coin_type.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/core/address_validator.dart'; @@ -408,16 +407,16 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e) { - if (e is LedgerException) { - final errorCode = e.errorCode.toRadixString(16); - final fallbackMsg = - e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode"; - final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg; - - state = FailureState(errorMsg); - } else { + // if (e is LedgerException) { + // final errorCode = e.errorCode.toRadixString(16); + // final fallbackMsg = + // e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode"; + // final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg; + // + // state = FailureState(errorMsg); + // } else { state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); - } + // } } return null; } diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 68bc95a00..91e0de685 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -13,7 +13,6 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:mobx/mobx.dart'; part 'wallet_hardware_restore_view_model.g.dart'; @@ -58,7 +57,11 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with switch (type) { case WalletType.bitcoin: accounts = await bitcoin! - .getHardwareWalletAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + .getHardwareWalletBitcoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + break; + case WalletType.litecoin: + accounts = await bitcoin! + .getHardwareWalletLitecoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); break; case WalletType.ethereum: accounts = await ethereum! @@ -74,9 +77,10 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with availableAccounts.addAll(accounts); _nextIndex += limit; - } on LedgerException catch (e) { - error = ledgerViewModel.interpretErrorCode(e.errorCode.toRadixString(16)); + // } on LedgerException catch (e) { + // error = ledgerViewModel.interpretErrorCode(e.errorCode.toRadixString(16)); } catch (e) { + print(e); error = S.current.ledger_connection_error; } @@ -89,6 +93,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with WalletCredentials credentials; switch (type) { case WalletType.bitcoin: + case WalletType.litecoin: credentials = bitcoin!.createBitcoinHardwareWalletCredentials(name: name, accountData: selectedAccount!); break; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 86b3462ac..9895e6305 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,6 +17,7 @@ import package_info_plus import path_provider_foundation import share_plus import shared_preferences_foundation +import universal_ble import url_launcher_macos import wakelock_plus @@ -33,6 +34,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index a2f19e596..d5fce76e9 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -12,7 +12,7 @@ dependencies: version: 4.0.2 shared_preferences: ^2.0.15 # provider: ^6.0.3 - rxdart: ^0.27.4 + rxdart: ^0.28.0 yaml: ^3.1.1 #barcode_scan: any barcode_scan2: ^4.2.1 @@ -97,7 +97,7 @@ dependencies: polyseed: ^0.0.6 nostr_tools: ^1.0.9 solana: ^0.30.1 - ledger_flutter: ^1.0.1 + ledger_flutter_plus: ^1.4.1 hashlib: ^1.19.2 dev_dependencies: @@ -125,10 +125,6 @@ dependency_overrides: bech32: git: url: https://github.com/cake-tech/bech32.git - ledger_flutter: - git: - url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-v3 web3dart: git: url: https://github.com/cake-tech/web3dart.git @@ -155,6 +151,7 @@ flutter: assets: - assets/images/ - assets/images/flags/ + - assets/images/hardware_wallet/ - assets/node_list.yml - assets/haven_node_list.yml - assets/bitcoin_electrum_server_list.yml diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 80433ae0e..2ec59f349 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -97,7 +97,7 @@ "cake_pay_account_note": "Melden Sie sich nur mit einer E-Mail-Adresse an, um Karten anzuzeigen und zu kaufen. Einige sind sogar mit Rabatt erhältlich!", "cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.", "cake_pay_save_order": "Die Karte sollte innerhalb von 1 Werktag an Ihre E-Mail gesendet werden, \n Ihre Bestell-ID zu speichern:", - "cake_pay_subtitle": "Kaufen Sie weltweite Prepaid -Karten und Geschenkkarten", + "cake_pay_subtitle": "Kaufen Sie weltweite Prepaid-Karten und Geschenkkarten", "cake_pay_web_cards_subtitle": "Kaufen Sie weltweit Prepaid-Karten und Geschenkkarten", "cake_pay_web_cards_title": "Cake Pay-Webkarten", "cake_wallet": "Cake Wallet", @@ -114,7 +114,7 @@ "change_currency": "Währung ändern", "change_current_node": "Möchten Sie den aktuellen Knoten wirklich zu ${node}? ändern?", "change_current_node_title": "Aktuellen Knoten ändern", - "change_exchange_provider": "Swap -Anbieter ändern", + "change_exchange_provider": "Swap-Anbieter ändern", "change_language": "Sprache ändern", "change_language_to": "Sprache zu ${language} ändern?", "change_password": "Passwort ändern", @@ -131,7 +131,7 @@ "choose_one": "Wähle ein", "choose_relay": "Bitte wählen Sie ein zu verwendendes Relais aus", "choose_wallet_currency": "Bitte wählen Sie die Währung der Wallet:", - "choose_wallet_group": "Wählen Sie Brieftaschengruppe", + "choose_wallet_group": "Wählen Sie Walletgruppe", "clear": "Zurücksetzen", "clearnet_link": "Clearnet-Link", "close": "Schließen", @@ -179,7 +179,7 @@ "create_invoice": "Rechnung erstellen", "create_new": "Neue Wallet erstellen", "create_new_account": "Neues Konto erstellen", - "create_new_seed": "Neue Samen erstellen", + "create_new_seed": "Neue Seed erstellen", "creating_new_wallet": "Neue Wallet erstellen", "creating_new_wallet_error": "Fehler: ${description}", "creation_date": "Erstellungsdatum", @@ -189,9 +189,9 @@ "custom_value": "Benutzerdefinierten Wert", "dark_theme": "Dunkel", "debit_card": "Debitkarte", - "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Geldbörse unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", + "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Wallet unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", "decimal_places_error": "Zu viele Nachkommastellen", - "decimals_cannot_be_zero": "Token -Dezimalzahl kann nicht Null sein.", + "decimals_cannot_be_zero": "Token-Dezimalzahl kann nicht Null sein.", "default_buy_provider": "Standard-Kaufanbieter", "default_sell_provider": "Standard-Verkaufsanbieter", "delete": "Löschen", @@ -236,7 +236,7 @@ "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", "email_address": "E-Mail-Adresse", "enable": "Aktivieren", - "enable_mempool_api": "Mempool -API für genaue Gebühren und Daten", + "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 stille Zahlungen, bis die Spitze erreicht ist", "enabled": "Ermöglicht", @@ -245,7 +245,7 @@ "enter_code": "Code eingeben", "enter_seed_phrase": "Geben Sie Ihre Seed-Phrase ein", "enter_totp_code": "Bitte geben Sie den TOTP-Code ein.", - "enter_wallet_password": "Geben Sie das Brieftaschenkennwort ein", + "enter_wallet_password": "Geben Sie das Walletkennwort ein", "enter_your_note": "Geben Sie Ihre Bemerkung ein…", "enter_your_pin": "PIN eingeben", "enter_your_pin_again": "Geben Sie Ihre PIN erneut ein", @@ -282,7 +282,7 @@ "event": "Ereignis", "events": "Veranstaltungen", "exchange": "Tauschen", - "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR aus Ihrer Cake -Wallet Monero -Balance austauschen möchten, wechseln Sie zuerst zu Ihrer Monero -Brieftasche.", + "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR aus Ihrer CakeWallet Monero-Balance tauschen möchten, wechseln Sie zuerst zu Ihrer Monero-Wallet.", "exchange_new_template": "Neue Vorlage", "exchange_provider_unsupported": "${providerName} wird nicht mehr unterstützt!", "exchange_result_confirm": "Durch Drücken von \"Bestätigen\" wird ${fetchingLabel} ${from} von Ihrer Wallet namens ${walletName} an die unten angegebene Adresse gesendet. Alternativ können Sie von einer externen Wallet an die unten angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu ändern.", @@ -309,7 +309,7 @@ "fill_code": "Geben Sie den Bestätigungscode ein, den Sie per E-Mail erhalten haben", "filter_by": "Filtern nach", "first_wallet_text": "Eine großartige Wallet für Monero, Bitcoin, Ethereum, Litecoin, und Haven", - "fixed_pair_not_supported": "Dieses feste Paar wird nicht von den ausgewählten Swap -Diensten unterstützt", + "fixed_pair_not_supported": "Dieses feste Paar wird nicht von den ausgewählten Swap-Diensten unterstützt", "fixed_rate": "Feste Rate", "fixed_rate_alert": "Sie können den Empfangsbetrag eingeben, wenn der Festratenmodus aktiviert ist. Möchten Sie in den Festratenmodus wechseln?", "forgot_password": "Passwort vergessen", @@ -349,9 +349,9 @@ "incoming": "Eingehend", "incorrect_seed": "Der eingegebene Text ist ungültig.", "inputs": "Eingänge", - "insufficient_lamport_for_tx": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Bitte fügen Sie Ihrer Brieftasche mehr Sol hinzu oder reduzieren Sie die SO -Menge, die Sie senden.", - "insufficient_lamports": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Sie brauchen mindestens ${solValueNeeded} Sol. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", - "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Brieftasche hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", + "insufficient_lamport_for_tx": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Bitte fügen Sie Ihrer Wallet mehr Sol hinzu oder reduzieren Sie die SOL-Menge, die Sie senden.", + "insufficient_lamports": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Sie brauchen mindestens ${solValueNeeded} Sol. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol-Betrag", + "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol-Betrag", "introducing_cake_pay": "Einführung von Cake Pay!", "invalid_input": "Ungültige Eingabe", "invalid_password": "Ungültiges Passwort", @@ -365,20 +365,20 @@ "ledger_error_wrong_app": "Bitte stellen Sie sicher, dass Sie die richtige App auf Ihrem Ledger geöffnet haben", "ledger_please_enable_bluetooth": "Bitte aktivieren Sie Bluetooth um sich mit Ihren Ledger zu verbinden.", "light_theme": "Hell", - "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB -Scannen", + "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB-Scannen", "litecoin_mweb": "MWeb", "litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen", - "litecoin_mweb_description": "MWEB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", + "litecoin_mweb_description": "MWWB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", "litecoin_mweb_dismiss": "Zurückweisen", - "litecoin_mweb_display_card": "MWEB -Karte anzeigen", + "litecoin_mweb_display_card": "MWEB-Karte anzeigen", "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Abstecken", "litecoin_mweb_scanning": "MWEB Scanning", - "litecoin_mweb_settings": "MWEB -Einstellungen", - "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin -Brieftaschen verfügbar", + "litecoin_mweb_settings": "MWEB-Einstellungen", + "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin-Wallets verfügbar", "litecoin_what_is_mweb": "Was ist MWeb?", - "live_fee_rates": "Live -Gebührenpreise über API", + "live_fee_rates": "Live-Gebührenpreise über API", "load_more": "Mehr laden", "loading_your_wallet": "Wallet wird geladen", "login": "Einloggen", @@ -533,8 +533,8 @@ "rename": "Umbenennen", "rep_warning": "Repräsentative Warnung", "rep_warning_sub": "Ihr Vertreter scheint nicht gut zu sein. Tippen Sie hier, um eine neue auszuwählen", - "repeat_wallet_password": "Wiederholen Sie das Brieftaschenkennwort", - "repeated_password_is_incorrect": "Wiederholtes Passwort ist falsch. Bitte wiederholen Sie das Brieftaschenkennwort erneut.", + "repeat_wallet_password": "Wiederholen Sie das Walletkennwort", + "repeated_password_is_incorrect": "Wiederholtes Passwort ist falsch. Bitte wiederholen Sie das Walletkennwort erneut.", "require_for_adding_contacts": "Erforderlich zum Hinzufügen von Kontakten", "require_for_all_security_and_backup_settings": "Für alle Sicherheits- und Sicherungseinstellungen erforderlich", "require_for_assessing_wallet": "Für den Zugriff auf die Wallet erforderlich", @@ -576,7 +576,7 @@ "restore_wallet": "Wallet wiederherstellen", "restore_wallet_name": "Walletname", "restore_wallet_restore_description": "Beschreibung zur Wallet-Wiederherstellung", - "robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrem Debitkarten-, Bankkonto- oder Robinhood -Guthaben. Nur USA.", + "robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrem Debitkarten-, Bankkonto- oder Robinhood-Guthaben. Nur USA.", "router_no_route": "Keine Route definiert für ${name}", "save": "Speichern", "save_backup_password": "Bitte stellen Sie sicher, dass Sie Ihr Sicherungskennwort gespeichert haben. Ohne dieses können Sie Ihre Sicherungsdateien nicht importieren.", @@ -622,8 +622,8 @@ "seed_share": "Seed teilen", "seed_title": "Seed", "seedtype": "Seedtyp", - "seedtype_alert_content": "Das Teilen von Samen mit anderen Brieftaschen ist nur mit bip39 Seedype möglich.", - "seedtype_alert_title": "Seedype -Alarm", + "seedtype_alert_content": "Das Teilen von Seeds mit anderen Wallet ist nur mit bip39 Seedype möglich.", + "seedtype_alert_title": "Seedype-Alarm", "seedtype_legacy": "Veraltet (25 Wörter)", "seedtype_polyseed": "Polyseed (16 Wörter)", "seedtype_wownero": "WOWNO (14 Wörter)", @@ -691,7 +691,7 @@ "setup_your_debit_card": "Richten Sie Ihre Debitkarte ein", "share": "Teilen", "share_address": "Adresse teilen ", - "shared_seed_wallet_groups": "Gemeinsame Samenbrieftaschengruppen", + "shared_seed_wallet_groups": "Gemeinsame Walletsseed Gruppen", "show_details": "Details anzeigen", "show_keys": "Seed/Schlüssel anzeigen", "show_market_place": "Marktplatz anzeigen", @@ -711,12 +711,12 @@ "silent_payments_disclaimer": "Neue Adressen sind keine neuen Identitäten. Es ist eine Wiederverwendung einer bestehenden Identität mit einem anderen Etikett.", "silent_payments_display_card": "Zeigen Sie stille Zahlungskarte", "silent_payments_scan_from_date": "Scan ab Datum", - "silent_payments_scan_from_date_or_blockheight": "Bitte geben Sie die Blockhöhe ein, die Sie für eingehende stille Zahlungen scannen möchten, oder verwenden Sie stattdessen das Datum. Sie können wählen, ob die Brieftasche jeden Block scannt oder nur die angegebene Höhe überprüft.", + "silent_payments_scan_from_date_or_blockheight": "Bitte geben Sie die Blockhöhe ein, die Sie für eingehende stille Zahlungen scannen möchten, oder verwenden Sie stattdessen das Datum. Sie können wählen, ob die Wallet jeden Block scannt oder nur die angegebene Höhe überprüft.", "silent_payments_scan_from_height": "Scan aus der Blockhöhe scannen", "silent_payments_scanned_tip": "Gescannt zum Trinkgeld! (${tip})", "silent_payments_scanning": "Stille Zahlungen scannen", "silent_payments_settings": "Einstellungen für stille Zahlungen", - "single_seed_wallets_group": "Einzelne Samenbriefen", + "single_seed_wallets_group": "Einzelne Wallets", "slidable": "Verschiebbar", "sort_by": "Sortiere nach", "spend_key_private": "Spend Key (geheim)", @@ -755,11 +755,11 @@ "syncing_wallet_alert_title": "Ihr Wallet wird synchronisiert", "template": "Vorlage", "template_name": "Vorlagenname", - "testnet_coins_no_value": "Testnet -Münzen haben keinen Wert", + "testnet_coins_no_value": "Testnet-Münzen haben keinen Wert", "third_intro_content": "Yats leben auch außerhalb von Cake Wallet. Jede Wallet-Adresse auf der Welt kann durch ein Yat ersetzt werden!", "third_intro_title": "Yat spielt gut mit anderen", "thorchain_contract_address_not_supported": "Thorchain unterstützt das Senden an eine Vertragsadresse nicht", - "thorchain_taproot_address_not_supported": "Der Thorchain -Anbieter unterstützt keine Taproot -Adressen. Bitte ändern Sie die Adresse oder wählen Sie einen anderen Anbieter aus.", + "thorchain_taproot_address_not_supported": "Der Thorchain-Anbieter unterstützt keine Taproot-Adressen. Bitte ändern Sie die Adresse oder wählen Sie einen anderen Anbieter aus.", "time": "${minutes}m ${seconds}s", "tip": "Hinweis:", "today": "Heute", @@ -879,12 +879,12 @@ "voting_weight": "Stimmgewicht", "waitFewSecondForTxUpdate": "Bitte warten Sie einige Sekunden, bis die Transaktion im Transaktionsverlauf angezeigt wird", "waiting_payment_confirmation": "Warte auf Zahlungsbestätigung", - "wallet_group": "Brieftaschengruppe", - "wallet_group_description_four": "eine Brieftasche mit einem völlig neuen Samen schaffen.", - "wallet_group_description_one": "In Kuchenbrieftasche können Sie eine erstellen", - "wallet_group_description_three": "Sehen Sie den Bildschirm zur verfügbaren Brieftaschen und/oder Brieftaschengruppen. Oder wählen", - "wallet_group_description_two": "Durch die Auswahl einer vorhandenen Brieftasche, mit der ein Samen geteilt werden kann. Jede Brieftaschengruppe kann eine einzelne Brieftasche jedes Währungstyps enthalten. \n\n Sie können auswählen", - "wallet_group_empty_state_text_one": "Sieht so aus, als hätten Sie keine kompatiblen Brieftaschengruppen !\n\n TAP", + "wallet_group": "Walletgruppe", + "wallet_group_description_four": "eine Wallet mit einem völlig neuen Seed schaffen.", + "wallet_group_description_one": "In CakeWallet können Sie eine erstellen", + "wallet_group_description_three": "Sehen Sie den Bildschirm zur verfügbaren Wallet und/oder Walletgruppen. Oder wählen", + "wallet_group_description_two": "Durch die Auswahl einer vorhandenen Wallet, mit der ein Seed geteilt werden kann. Jede Walletgruppe kann eine einzelne Wallet jedes Währungstyps enthalten. \n\n Sie können auswählen", + "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_keys": "Wallet-Seed/-Schlüssel", "wallet_list_create_new_wallet": "Neue Wallet erstellen", @@ -901,7 +901,7 @@ "wallet_menu": "Wallet-Menü", "wallet_name": "Walletname", "wallet_name_exists": "Wallet mit diesem Namen existiert bereits", - "wallet_password_is_empty": "Brieftaschenkennwort ist leer. Brieftaschenkennwort sollte nicht leer sein", + "wallet_password_is_empty": "Walletkennwort ist leer. Walletkennwort sollte nicht leer sein", "wallet_recovery_height": "Erstellungshöhe", "wallet_restoration_store_incorrect_seed_length": "Falsche Seed-Länge", "wallet_seed": "Wallet-Seed", @@ -941,4 +941,4 @@ "you_will_get": "Konvertieren zu", "you_will_send": "Konvertieren von", "yy": "YY" -} \ No newline at end of file +} diff --git a/tool/configure.dart b/tool/configure.dart index 704b47526..97541c2fa 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -97,7 +97,7 @@ import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bip39/bip39.dart' as bip39; """; @@ -121,6 +121,7 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart'; +import 'package:cw_bitcoin/litecoin_hardware_wallet_service.dart'; import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; @@ -223,8 +224,9 @@ abstract class Bitcoin { void deleteSilentPaymentAddress(Object wallet, String address); Future updateFeeRates(Object wallet); int getMaxCustomFeeRate(Object wallet); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); - Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); + Future> getHardwareWalletBitcoinAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + Future> getHardwareWalletLitecoinAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); List updateOutputs(PendingTransaction pendingTransaction, List outputs); bool txIsReceivedSilentPayment(TransactionInfo txInfo); bool txIsMweb(TransactionInfo txInfo); @@ -819,7 +821,7 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:web3dart/web3dart.dart'; """; @@ -885,7 +887,7 @@ abstract class Ethereum { Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); } """; @@ -923,7 +925,7 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:web3dart/web3dart.dart'; """; @@ -989,7 +991,7 @@ abstract class Polygon { Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); } """; diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c6444e09c..ad540a359 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UniversalBlePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UniversalBlePluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0a0b2f9eb..92431a6fb 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows permission_handler_windows share_plus + universal_ble url_launcher_windows ) From 4372165b261bcb52818e56966e1295a24e1cf2a2 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 24 Oct 2024 04:20:25 +0100 Subject: [PATCH 139/142] fix: Issue with prevent screenshots implementation on iOS (#1729) --- ios/Runner/AppDelegate.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 402f6556b..477d47f7b 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -110,12 +110,12 @@ import workmanager private func makeSecure() { if (!self.window.subviews.contains(textField)) { + let view = UIView(frame: CGRect(x: 0, y: 0, width: textField.frame.self.width, height: textField.frame.self.height)) self.window.addSubview(textField) - textField.centerYAnchor.constraint(equalTo: self.window.centerYAnchor).isActive = true - textField.centerXAnchor.constraint(equalTo: self.window.centerXAnchor).isActive = true self.window.layer.superlayer?.addSublayer(textField.layer) - textField.layer.sublayers?.first?.addSublayer(self.window.layer) + textField.layer.sublayers?.last!.addSublayer(self.window.layer) + textField.leftView = view + textField.leftViewMode = .always } } - -} +} \ No newline at end of file From 858e5cac523583ee37255b7eb033d13946686593 Mon Sep 17 00:00:00 2001 From: rottenwheel <92872541+rottenwheel@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:56:02 +0000 Subject: [PATCH 140/142] spanish: miscellaneous rewording (#1777) --- res/values/strings_es.arb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index a8ad7549a..520c8ea78 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -78,7 +78,7 @@ "billing_address_info": "Si se le solicita una dirección de facturación, proporcione su dirección de envío", "biometric_auth_reason": "Escanee su huella digital para autenticar", "bitcoin_dark_theme": "Tema oscuro de Bitcoin", - "bitcoin_light_theme": "Tema de la luz de Bitcoin", + "bitcoin_light_theme": "Tema claro de Bitcoin", "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por su paciencia! Se le enviará un correo electrónico cuando se confirme el pago.", "block_remaining": "1 bloqueo restante", "Blocks_remaining": "${status} Bloques restantes", @@ -93,7 +93,7 @@ "buy_with": "Compra con", "by_cake_pay": "por Cake Pay", "cake_2fa_preset": "Pastel 2FA preestablecido", - "cake_dark_theme": "Tema oscuro del pastel", + "cake_dark_theme": "Tema oscuro", "cake_pay_account_note": "Regístrese con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", "cake_pay_save_order": "La tarjeta debe enviarse a su correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:", @@ -144,9 +144,9 @@ "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Desea continuar?", "confirm_fee_deduction": "Confirmar la deducción de la tarifa", "confirm_fee_deduction_content": "¿Acepta deducir la tarifa de la producción?", - "confirm_passphrase": "Confirmar la frase de pases", + "confirm_passphrase": "Confirmar la contraseña", "confirm_sending": "Confirmar envío", - "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ ncake billet cambiará a un nodo compatible, solo para escanear", + "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ nCake cambiará a un nodo compatible, solo para escanear", "confirmations": "Confirmaciones", "confirmed": "Saldo confirmado", "confirmed_tx": "Confirmado", @@ -281,7 +281,7 @@ "etherscan_history": "historia de etherscan", "event": "Evento", "events": "Eventos", - "exchange": "Intercambio", + "exchange": "Intercambiar", "exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR desde su billetera de pastel Monero Balance, primero cambie a su billetera Monero.", "exchange_new_template": "Nueva plantilla", "exchange_provider_unsupported": "¡${providerName} ya no es compatible!", @@ -826,7 +826,7 @@ "transaction_priority_slow": "Lento", "transaction_sent": "Transacción enviada!", "transaction_sent_notice": "Si la pantalla no continúa después de 1 minuto, revisa un explorador de bloques y tu correo electrónico.", - "transactions": "Actas", + "transactions": "Transacciones", "transactions_by_date": "Transacciones por fecha", "trongrid_history": "Historia trongrid", "trusted": "de confianza", @@ -939,4 +939,4 @@ "you_will_get": "Convertir a", "you_will_send": "Convertir de", "yy": "YY" -} \ No newline at end of file +} From 54c41d41cd1cd3dec59fafad9fd56eeb0bda671b Mon Sep 17 00:00:00 2001 From: rottenwheel <92872541+rottenwheel@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:56:21 +0000 Subject: [PATCH 141/142] spanish (#1776) --- res/values/strings_es.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 520c8ea78..449557671 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -372,8 +372,8 @@ "litecoin_mweb_dismiss": "Despedir", "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", "litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.", - "litecoin_mweb_pegin": "Meter", - "litecoin_mweb_pegout": "Estirar la pata", + "litecoin_mweb_pegin": "Convertir", + "litecoin_mweb_pegout": "Recuperar", "litecoin_mweb_scanning": "Escaneo mweb", "litecoin_mweb_settings": "Configuración de MWEB", "litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin", From 5c206a8b7cf2a9279910a7c97bf2689099a26f0a Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 28 Oct 2024 02:37:38 +0200 Subject: [PATCH 142/142] fix MobX Exception: computed values issue (#1763) --- cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 2686d12cc..c29579436 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -331,7 +331,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ); silentAddresses.add(address); - updateAddressesByMatch(); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } @@ -348,7 +348,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { network: network, ); _addresses.add(address); - updateAddressesByMatch(); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; }