From 76322a17774a0ce7dfcbfd10e1675b545ab9ccd1 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Fri, 4 Aug 2023 13:18:27 +0200 Subject: [PATCH 1/5] CW-392 Allow Users to rename Monero and Haven accounts (#1008) --- .../monero_accounts/widgets/account_tile.dart | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/lib/src/screens/monero_accounts/widgets/account_tile.dart b/lib/src/screens/monero_accounts/widgets/account_tile.dart index b0769e9c2..d034ca11a 100644 --- a/lib/src/screens/monero_accounts/widgets/account_tile.dart +++ b/lib/src/screens/monero_accounts/widgets/account_tile.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:cake_wallet/generated/i18n.dart'; class AccountTile extends StatelessWidget { AccountTile( @@ -19,16 +21,17 @@ class AccountTile extends StatelessWidget { @override Widget build(BuildContext context) { final color = isCurrent - ? Theme.of(context).textTheme!.titleSmall!.decorationColor! - : Theme.of(context).textTheme!.displayLarge!.decorationColor!; + ? Theme.of(context).textTheme.titleSmall!.decorationColor! + : Theme.of(context).textTheme.displayLarge!.decorationColor!; final textColor = isCurrent - ? Theme.of(context).textTheme!.titleSmall!.color! - : Theme.of(context).textTheme!.displayLarge!.color!; + ? Theme.of(context).textTheme.titleSmall!.color! + : Theme.of(context).textTheme.displayLarge!.color!; final Widget cell = GestureDetector( onTap: onTap, child: Container( height: 77, + width: double.infinity, padding: EdgeInsets.only(left: 24, right: 24), color: color, child: Wrap( @@ -58,7 +61,7 @@ class AccountTile extends StatelessWidget { fontSize: 15, fontWeight: FontWeight.w600, fontFamily: 'Lato', - color: Theme.of(context).textTheme!.headlineMedium!.color!, + color: Theme.of(context).textTheme.headlineMedium!.color!, decoration: TextDecoration.none, ), ), @@ -67,18 +70,26 @@ class AccountTile extends StatelessWidget { ), ), ); - // FIX-ME: Splidable - return cell; - // return Slidable( - // key: Key(accountName), - // child: cell, - // actionPane: SlidableDrawerActionPane(), - // secondaryActions: [ - // IconSlideAction( - // caption: S.of(context).edit, - // color: Colors.blue, - // icon: Icons.edit, - // onTap: () => onEdit?.call()) - // ]); + + // return cell; + return Slidable( + key: Key(accountName), + child: cell, + endActionPane: _actionPane(context) + ); } + + ActionPane _actionPane(BuildContext context) => ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.3, + children: [ + SlidableAction( + onPressed: (_) => onEdit.call(), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: Icons.edit, + label: S.of(context).edit, + ), + ], + ); } From e9df03b49b3fb8b06222ee81b5ab1f27c83d02ff Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:27:19 +0100 Subject: [PATCH 2/5] feat: Introduce QR Scanning to add new node (#1007) * feat: Introduce QR Scanning to add new node * fix: Exception handing for scan qr and invalid qr value * fix: Exception handing for scan qr and invalid qr value --- .../nodes/node_create_or_edit_page.dart | 13 ++++++ lib/src/screens/nodes/widgets/node_form.dart | 11 +++++ .../node_create_or_edit_view_model.dart | 42 ++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart index 216238b54..c1b07d8c5 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -66,6 +66,19 @@ class NodeCreateOrEditPage extends BasePage { @override String get title => editingNode != null ? S.current.edit_node : S.current.node_new; + @override + Widget trailing(BuildContext context) => IconButton( + onPressed: () async { + await nodeCreateOrEditViewModel.scanQRCodeForNewNode(); + }, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + icon: Image.asset( + 'assets/images/qr_code_icon.png', + ), + ); + final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; final Node? editingNode; final bool? isSelected; diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 8ee3ecd2f..91974fce5 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -44,6 +44,17 @@ class NodeForm extends StatelessWidget { } }); } + reaction((_) => nodeViewModel.address, (String address) { + if (address != _addressController.text) { + _addressController.text = address; + } + }); + + reaction((_) => nodeViewModel.port, (String port) { + if (port != _portController.text) { + _portController.text = port; + } + }); _addressController.addListener(() => nodeViewModel.address = _addressController.text); _portController.addListener(() => nodeViewModel.port = _portController.text); diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 4433eb5b6..f749ed0d5 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -8,10 +9,12 @@ import 'package:collection/collection.dart'; part 'node_create_or_edit_view_model.g.dart'; -class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase with _$NodeCreateOrEditViewModel; +class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase + with _$NodeCreateOrEditViewModel; abstract class NodeCreateOrEditViewModelBase with Store { - NodeCreateOrEditViewModelBase(this._nodeSource, this._walletType, this._settingsStore) + NodeCreateOrEditViewModelBase( + this._nodeSource, this._walletType, this._settingsStore) : state = InitialExecutionState(), connectionState = InitialExecutionState(), useSSL = false, @@ -170,4 +173,39 @@ abstract class NodeCreateOrEditViewModelBase with Store { @action void setAsCurrent(Node node) => _settingsStore.nodes[_walletType] = node; + + @action + Future scanQRCodeForNewNode() async { + try { + String code = await presentQRScanner(); + + if (code.isEmpty) { + throw Exception('Unexpected scan QR code value: value is empty'); + } + + final uri = Uri.tryParse(code); + + if (uri == null) { + throw Exception('Unexpected scan QR code value: Value is invalid'); + } + + final userInfo = uri.userInfo.split(':'); + + if (userInfo.length < 2) { + throw Exception('Unexpected scan QR code value: Value is invalid'); + } + + final rpcUser = userInfo[0]; + final rpcPassword = userInfo[1]; + final ipAddress = uri.host; + final port = uri.port.toString(); + + setAddress(ipAddress); + setPassword(rpcPassword); + setLogin(rpcUser); + setPort(port); + } on Exception catch (e) { + connectionState = FailureState(e.toString()); + } + } } From 412039412131fb92e9e141550f1bd5f519e4291b Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:49:26 +0100 Subject: [PATCH 3/5] CW-266 verbose access controls for TOTP 2FA (#967) * chore: Setup * feat: Verbose controls for TOTP 2FA WIP [skip-ci] * feat: Implement verbose controls for sends to contact, non contacts and internal wallets * feat: Implement verbose 2FA control for exchanges to internal wallets [skip-ci] * Implement verbose controls * chore: PR cleanup * fix: Implement fixes and recommendations on verbose controls * feat: Localization for verbose controls settings * fix: disable pin when 2fa is not activated * fix: Naming error * chore: Reformat code with linelength of 100 * fix: Wallet type page and type bug when creating wallet * fix: add new values to be stored in local storage to both reload function and import/export functions in back_service.dart * fix: White spaces with localization files * fix: Switch observers in modify_2fa page to individual observer * chore: Switch custom tab widget to reusable SettingsChoicesCell widget * chore: Remove unneeded argument in create wallet entrypoint * fix: Switch type for selectedCakePreference when importing preferences from backup file * fix: Await all values being saved to local storage --------- Co-authored-by: David Adegoke --- .gitignore | 1 + android/gradle.properties | 2 +- lib/core/auth_service.dart | 33 +- lib/core/backup_service.dart | 318 +++++------ lib/di.dart | 48 +- lib/entities/cake_2fa_preset_options.dart | 35 ++ lib/entities/preferences_key.dart | 28 +- .../screens/contact/contact_list_page.dart | 269 +++++----- .../desktop_wallet_selection_dropdown.dart | 22 +- lib/src/screens/exchange/exchange_page.dart | 494 ++++++++---------- lib/src/screens/root/root.dart | 20 +- lib/src/screens/send/send_page.dart | 52 +- .../settings/security_backup_page.dart | 23 +- .../widgets/settings_choices_cell.dart | 17 +- .../screens/setup_2fa/modify_2fa_page.dart | 177 +++++-- lib/src/screens/setup_2fa/setup_2fa.dart | 3 +- .../setup_2fa/setup_2fa_enter_code_page.dart | 50 +- lib/src/screens/wallet/wallet_edit_page.dart | 4 +- .../screens/wallet_list/wallet_list_page.dart | 102 ++-- lib/store/settings_store.dart | 163 ++++++ .../contact_list/contact_list_view_model.dart | 22 +- .../exchange/exchange_view_model.dart | 234 ++++++--- lib/view_model/send/send_view_model.dart | 110 +++- lib/view_model/set_up_2fa_viewmodel.dart | 262 ++++++++++ .../security_settings_view_model.dart | 4 + .../wallet_list/wallet_list_view_model.dart | 9 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + res/values/strings_ar.arb | 12 + res/values/strings_bg.arb | 12 + res/values/strings_cs.arb | 12 + res/values/strings_de.arb | 12 + res/values/strings_en.arb | 12 + res/values/strings_es.arb | 12 + res/values/strings_fr.arb | 12 + res/values/strings_ha.arb | 13 +- res/values/strings_hi.arb | 12 + res/values/strings_hr.arb | 12 + res/values/strings_id.arb | 12 + res/values/strings_it.arb | 12 + res/values/strings_ja.arb | 12 + res/values/strings_ko.arb | 12 + res/values/strings_my.arb | 12 + res/values/strings_nl.arb | 12 + res/values/strings_pl.arb | 12 + res/values/strings_pt.arb | 12 + res/values/strings_ru.arb | 12 + res/values/strings_th.arb | 12 + res/values/strings_tr.arb | 13 + res/values/strings_uk.arb | 12 + res/values/strings_ur.arb | 12 + res/values/strings_yo.arb | 12 + res/values/strings_zh.arb | 12 + 52 files changed, 1974 insertions(+), 832 deletions(-) create mode 100644 lib/entities/cake_2fa_preset_options.dart diff --git a/.gitignore b/.gitignore index 9fb7fd204..70d99f753 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +.fvm/ # IntelliJ related *.iml diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d4544..a5965ab8d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.enableR8=true android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=true \ No newline at end of file diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 8091740e6..854640015 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -25,6 +25,10 @@ class AuthService with Store { Routes.setupPin, Routes.setup_2faPage, Routes.modify2FAPage, + Routes.newWallet, + Routes.newWalletType, + Routes.addressBookAddContact, + Routes.restoreOptions, ]; final FlutterSecureStorage secureStorage; @@ -81,21 +85,26 @@ class AuthService with Store { } Future authenticateAction(BuildContext context, - {Function(bool)? onAuthSuccess, String? route, Object? arguments}) async { + {Function(bool)? onAuthSuccess, + String? route, + Object? arguments, + required bool conditionToDetermineIfToUse2FA}) async { assert(route != null || onAuthSuccess != null, 'Either route or onAuthSuccess param must be passed.'); - if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { - if (onAuthSuccess != null) { - onAuthSuccess(true); - } else { - Navigator.of(context).pushNamed( - route ?? '', - arguments: arguments, - ); + if (!conditionToDetermineIfToUse2FA) { + if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { + if (onAuthSuccess != null) { + onAuthSuccess(true); + } else { + Navigator.of(context).pushNamed( + route ?? '', + arguments: arguments, + ); + } + return; } - return; - } +} Navigator.of(context).pushNamed(Routes.auth, @@ -104,7 +113,7 @@ class AuthService with Store { onAuthSuccess?.call(false); return; } else { - if (settingsStore.useTOTP2FA) { + if (settingsStore.useTOTP2FA && conditionToDetermineIfToUse2FA) { auth.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 2870c4488..3f3eedd57 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; @@ -19,8 +20,8 @@ import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cake_backup/backup.dart' as cake_backup; class BackupService { - BackupService(this._flutterSecureStorage, this._walletInfoSource, - this._keyService, this._sharedPreferences) + BackupService( + this._flutterSecureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = []; @@ -67,9 +68,8 @@ class BackupService { } @Deprecated('Use v2 instead') - Future _exportBackupV1(String password, - {String nonce = secrets.backupSalt}) async - => throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); + Future _exportBackupV1(String password, {String nonce = secrets.backupSalt}) async => + throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); Future _exportBackupV2(String password) async { final zipEncoder = ZipFileEncoder(); @@ -112,8 +112,7 @@ class BackupService { return await _encryptV2(content, password); } - Future _importBackupV1(Uint8List data, String password, - {required String nonce}) async { + Future _importBackupV1(Uint8List data, String password, {required String nonce}) async { final appDir = await getApplicationDocumentsDirectory(); final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); @@ -161,10 +160,8 @@ class BackupService { Future _verifyWallets() async { final walletInfoSource = await _reloadHiveWalletInfoBox(); - _correctWallets = walletInfoSource - .values - .where((info) => availableWalletTypes.contains(info.type)) - .toList(); + _correctWallets = + walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList(); if (_correctWallets.isEmpty) { throw Exception('Correct wallets not detected'); @@ -191,14 +188,12 @@ class BackupService { return; } - final data = - json.decode(preferencesFile.readAsStringSync()) as Map; + final data = json.decode(preferencesFile.readAsStringSync()) as Map; String currentWalletName = data[PreferencesKey.currentWalletName] as String; int currentWalletType = data[PreferencesKey.currentWalletType] as int; final isCorrentCurrentWallet = _correctWallets - .any((info) => info.name == currentWalletName && - info.type.index == currentWalletType); + .any((info) => info.name == currentWalletName && info.type.index == currentWalletType); if (!isCorrentCurrentWallet) { currentWalletName = _correctWallets.first.name; @@ -212,138 +207,173 @@ class BackupService { final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; final disableBuy = data[PreferencesKey.disableBuyKey] as bool?; final disableSell = data[PreferencesKey.disableSellKey] as bool?; - final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; - final allowBiometricalAuthentication = data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; - final currentBitcoinElectrumSererId = data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; + final currentTransactionPriorityKeyLegacy = + data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; + final allowBiometricalAuthentication = + data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; + final currentBitcoinElectrumSererId = + data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?; final displayActionListMode = data[PreferencesKey.displayActionListModeKey] as int?; final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?; final currentPinLength = data[PreferencesKey.currentPinLength] as int?; final currentTheme = data[PreferencesKey.currentTheme] as int?; final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?; - final currentDefaultSettingsMigrationVersion = data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; + final currentDefaultSettingsMigrationVersion = + data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?; final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?; + final selectedCake2FAPreset = data[PreferencesKey.selectedCake2FAPreset] as int?; + final shouldRequireTOTP2FAForAccessingWallet = + data[PreferencesKey.shouldRequireTOTP2FAForAccessingWallet] as bool?; + final shouldRequireTOTP2FAForSendsToContact = + data[PreferencesKey.shouldRequireTOTP2FAForSendsToContact] as bool?; + final shouldRequireTOTP2FAForSendsToNonContact = + data[PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact] as bool?; + final shouldRequireTOTP2FAForSendsToInternalWallets = + data[PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets] as bool?; + final shouldRequireTOTP2FAForExchangesToInternalWallets = + data[PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets] as bool?; + final shouldRequireTOTP2FAForAddingContacts = + data[PreferencesKey.shouldRequireTOTP2FAForAddingContacts] as bool?; + final shouldRequireTOTP2FAForCreatingNewWallets = + data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?; + final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?; - await _sharedPreferences.setString(PreferencesKey.currentWalletName, - currentWalletName); + await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); if (currentNodeId != null) - await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, - currentNodeId); + await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId); if (currentBalanceDisplayMode != null) - await _sharedPreferences.setInt(PreferencesKey.currentBalanceDisplayModeKey, - currentBalanceDisplayMode); + await _sharedPreferences.setInt( + PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode); - await _sharedPreferences.setInt(PreferencesKey.currentWalletType, - currentWalletType); + await _sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType); if (currentFiatCurrency != null) - await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey, - currentFiatCurrency); + await _sharedPreferences.setString( + PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency); if (shouldSaveRecipientAddress != null) await _sharedPreferences.setBool( - PreferencesKey.shouldSaveRecipientAddressKey, - shouldSaveRecipientAddress); + PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress); if (isAppSecure != null) - await _sharedPreferences.setBool( - PreferencesKey.isAppSecureKey, - isAppSecure); + await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); if (disableBuy != null) - await _sharedPreferences.setBool( - PreferencesKey.disableBuyKey, - disableBuy); + await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy); if (disableSell != null) - await _sharedPreferences.setBool( - PreferencesKey.disableSellKey, - disableSell); + await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell); if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( - PreferencesKey.currentTransactionPriorityKeyLegacy, - currentTransactionPriorityKeyLegacy); + PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); if (allowBiometricalAuthentication != null) await _sharedPreferences.setBool( - PreferencesKey.allowBiometricalAuthenticationKey, - allowBiometricalAuthentication); + PreferencesKey.allowBiometricalAuthenticationKey, allowBiometricalAuthentication); if (currentBitcoinElectrumSererId != null) await _sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - currentBitcoinElectrumSererId); + PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId); if (currentLanguageCode != null) - await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, - currentLanguageCode); + await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode); if (displayActionListMode != null) - await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey, - displayActionListMode); + await _sharedPreferences.setInt( + PreferencesKey.displayActionListModeKey, displayActionListMode); if (fiatApiMode != null) - await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, - fiatApiMode); + await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); if (currentPinLength != null) - await _sharedPreferences.setInt(PreferencesKey.currentPinLength, - currentPinLength); + await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); if (currentTheme != null) - await _sharedPreferences.setInt( - PreferencesKey.currentTheme, currentTheme); + await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme); if (exchangeStatus != null) - await _sharedPreferences.setInt( - PreferencesKey.exchangeStatusKey, exchangeStatus); + await _sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus); if (currentDefaultSettingsMigrationVersion != null) - await _sharedPreferences.setInt( - PreferencesKey.currentDefaultSettingsMigrationVersion, - currentDefaultSettingsMigrationVersion); + await _sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, + currentDefaultSettingsMigrationVersion); if (moneroTransactionPriority != null) - await _sharedPreferences.setInt(PreferencesKey.moneroTransactionPriority, - moneroTransactionPriority); + await _sharedPreferences.setInt( + PreferencesKey.moneroTransactionPriority, moneroTransactionPriority); if (bitcoinTransactionPriority != null) - await _sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority, - bitcoinTransactionPriority); + await _sharedPreferences.setInt( + PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority); + + if (selectedCake2FAPreset != null) + await _sharedPreferences.setInt(PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset); + + if (shouldRequireTOTP2FAForAccessingWallet != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet, + shouldRequireTOTP2FAForAccessingWallet); + + if (shouldRequireTOTP2FAForSendsToContact != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact, + shouldRequireTOTP2FAForSendsToContact); + + if (shouldRequireTOTP2FAForSendsToNonContact != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact, + shouldRequireTOTP2FAForSendsToNonContact); + + if (shouldRequireTOTP2FAForSendsToInternalWallets != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets, + shouldRequireTOTP2FAForSendsToInternalWallets); + + if (shouldRequireTOTP2FAForExchangesToInternalWallets != null) + await _sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + shouldRequireTOTP2FAForExchangesToInternalWallets); + + if (shouldRequireTOTP2FAForAddingContacts != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts, + shouldRequireTOTP2FAForAddingContacts); + + if (shouldRequireTOTP2FAForCreatingNewWallets != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets, + shouldRequireTOTP2FAForCreatingNewWallets); + + if (shouldRequireTOTP2FAForAllSecurityAndBackupSettings != null) + await _sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + shouldRequireTOTP2FAForAllSecurityAndBackupSettings); await preferencesFile.delete(); } Future _importKeychainDumpV1(String password, - {required String nonce, - String keychainSalt = secrets.backupKeychainSalt}) async { + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); - final decryptedKeychainDumpFileData = await _decryptV1( - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) - as Map; + final decryptedKeychainDumpFileData = + await _decryptV1(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); + final keychainJSON = + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; final keychainWalletsInfo = keychainJSON['wallets'] as List; final decodedPin = keychainJSON['pin'] as String; final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.write( - key: backupPasswordKey, value: backupPassword); + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.write( - key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -352,27 +382,24 @@ class BackupService { {String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); - final decryptedKeychainDumpFileData = await _decryptV2( - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) - as Map; + final decryptedKeychainDumpFileData = + await _decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); + final keychainJSON = + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; final keychainWalletsInfo = keychainJSON['wallets'] as List; final decodedPin = keychainJSON['pin'] as String; final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.write( - key: backupPasswordKey, value: backupPassword); + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.write( - key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -386,35 +413,26 @@ class BackupService { @Deprecated('Use v2 instead') Future _exportKeychainDumpV1(String password, - {required String nonce, - String keychainSalt = secrets.backupKeychainSalt}) async - => throw Exception('Deprecated'); + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async => + throw Exception('Deprecated'); Future _exportKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPin = await _flutterSecureStorage.read(key: key); final decodedPin = decodedPinCode(pin: encodedPin!); - final wallets = - await Future.wait(_walletInfoSource.values.map((walletInfo) async { + final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), - 'password': - await _keyService.getWalletPassword(walletName: walletInfo.name) + 'password': await _keyService.getWalletPassword(walletName: walletInfo.name) }; })); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); - final backupPassword = - await _flutterSecureStorage.read(key: backupPasswordKey); - final data = utf8.encode(json.encode({ - 'pin': decodedPin, - 'wallets': wallets, - backupPasswordKey: backupPassword - })); - final encrypted = await _encryptV2( - Uint8List.fromList(data), '$keychainSalt$password'); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPassword = await _flutterSecureStorage.read(key: backupPasswordKey); + final data = utf8.encode( + json.encode({'pin': decodedPin, 'wallets': wallets, backupPasswordKey: backupPassword})); + final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password'); return encrypted; } @@ -423,46 +441,57 @@ class BackupService { final preferences = { PreferencesKey.currentWalletName: _sharedPreferences.getString(PreferencesKey.currentWalletName), - PreferencesKey.currentNodeIdKey: - _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), - PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences - .getInt(PreferencesKey.currentBalanceDisplayModeKey), - PreferencesKey.currentWalletType: - _sharedPreferences.getInt(PreferencesKey.currentWalletType), + PreferencesKey.currentNodeIdKey: _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), + PreferencesKey.currentBalanceDisplayModeKey: + _sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey), + PreferencesKey.currentWalletType: _sharedPreferences.getInt(PreferencesKey.currentWalletType), PreferencesKey.currentFiatCurrencyKey: _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), - PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences - .getBool(PreferencesKey.shouldSaveRecipientAddressKey), - PreferencesKey.disableBuyKey: _sharedPreferences - .getBool(PreferencesKey.disableBuyKey), - PreferencesKey.disableSellKey: _sharedPreferences - .getBool(PreferencesKey.disableSellKey), + PreferencesKey.shouldSaveRecipientAddressKey: + _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), + PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey), + PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey), PreferencesKey.isDarkThemeLegacy: _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy), - PreferencesKey.currentPinLength: - _sharedPreferences.getInt(PreferencesKey.currentPinLength), - PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences - .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), - PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences - .getBool(PreferencesKey.allowBiometricalAuthenticationKey), - PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences - .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), + PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), + PreferencesKey.currentTransactionPriorityKeyLegacy: + _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), + PreferencesKey.allowBiometricalAuthenticationKey: + _sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey), + PreferencesKey.currentBitcoinElectrumSererIdKey: + _sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), PreferencesKey.currentLanguageCode: _sharedPreferences.getString(PreferencesKey.currentLanguageCode), PreferencesKey.displayActionListModeKey: _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), - PreferencesKey.currentTheme: - _sharedPreferences.getInt(PreferencesKey.currentTheme), - PreferencesKey.exchangeStatusKey: - _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), - PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences - .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), + PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme), + PreferencesKey.exchangeStatusKey: _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), + PreferencesKey.currentDefaultSettingsMigrationVersion: + _sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), PreferencesKey.bitcoinTransactionPriority: _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority), PreferencesKey.moneroTransactionPriority: _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), PreferencesKey.currentFiatApiModeKey: - _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), + _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), + PreferencesKey.selectedCake2FAPreset: + _sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset), + PreferencesKey.shouldRequireTOTP2FAForAccessingWallet: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet), + PreferencesKey.shouldRequireTOTP2FAForSendsToContact: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact), + PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact), + PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets), + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets: _sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets), + PreferencesKey.shouldRequireTOTP2FAForAddingContacts: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts), + PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets), + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), }; return json.encode(preferences); @@ -476,28 +505,23 @@ class BackupService { } @Deprecated('Use v2 instead') - Future _encryptV1( - Uint8List data, String secretKeySource, String nonceBase64) async - => throw Exception('Deprecated'); + Future _encryptV1(Uint8List data, String secretKeySource, String nonceBase64) async => + throw Exception('Deprecated'); - Future _decryptV1( - Uint8List data, String secretKeySource, String nonceBase64, {int macLength = 16}) async { + Future _decryptV1(Uint8List data, String secretKeySource, String nonceBase64, + {int macLength = 16}) async { final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); final secretKey = SecretKey(secretKeyHash.bytes); final nonce = base64.decode(nonceBase64).toList(); - final box = SecretBox( - Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(), - nonce: nonce, - mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength))); + final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(), + nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength))); final plainData = await _cipher.decrypt(box, secretKey: secretKey); return Uint8List.fromList(plainData); } - Future _encryptV2( - Uint8List data, String passphrase) async - => cake_backup.encrypt(passphrase, data, version: _v2); + Future _encryptV2(Uint8List data, String passphrase) async => + cake_backup.encrypt(passphrase, data, version: _v2); - Future _decryptV2( - Uint8List data, String passphrase) async - => cake_backup.decrypt(passphrase, data); + Future _decryptV2(Uint8List data, String passphrase) async => + cake_backup.decrypt(passphrase, data); } diff --git a/lib/di.dart b/lib/di.dart index b7b012925..f287b0e9a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -247,7 +247,9 @@ Future setup({ nodeSource: _nodeSource, isBitcoinBuyEnabled: isBitcoinBuyEnabled, // Enforce darkTheme on platforms other than mobile till the design for other themes is completed - initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile ? null : ThemeList.darkTheme, + initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile + ? null + : ThemeList.darkTheme, ); if (_isSetupFinished) { @@ -389,7 +391,9 @@ Future setup({ final authStore = getIt.get(); final appStore = getIt.get(); final useTotp = appStore.settingsStore.useTOTP2FA; - if (useTotp) { + final shouldUseTotp2FAToAccessWallets = + appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; + if (useTotp && shouldUseTotp2FAToAccessWallets) { authPageState.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( @@ -525,17 +529,22 @@ Future setup({ getIt.get(), getIt.get())); - getIt.registerFactory(() => SendViewModel( + getIt.registerFactory( + () => SendViewModel( getIt.get().wallet!, getIt.get().settingsStore, getIt.get(), getIt.get(), getIt.get(), - _transactionDescriptionBox)); + getIt.get(), + _transactionDescriptionBox, + ), + ); getIt.registerFactoryParam( (PaymentRequest? initialPaymentRequest, _) => SendPage( sendViewModel: getIt.get(), + authService: getIt.get(), initialPaymentRequest: initialPaymentRequest, )); @@ -570,8 +579,8 @@ Future setup({ )); getIt.registerFactoryParam( - (WalletListViewModel walletListViewModel, _) => WalletEditViewModel( - walletListViewModel, getIt.get())); + (WalletListViewModel walletListViewModel, _) => + WalletEditViewModel(walletListViewModel, getIt.get())); getIt.registerFactoryParam, void>((args, _) { final walletListViewModel = args.first as WalletListViewModel; @@ -583,7 +592,6 @@ Future setup({ editingWallet: editingWallet); }); - getIt.registerFactory(() { final wallet = getIt.get().wallet!; @@ -654,10 +662,11 @@ Future setup({ (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact)); getIt.registerFactoryParam( - (CryptoCurrency? cur, _) => ContactListViewModel(_contactSource, _walletInfoSource, cur)); + (CryptoCurrency? cur, _) => + ContactListViewModel(_contactSource, _walletInfoSource, cur, getIt.get())); - getIt.registerFactoryParam( - (CryptoCurrency? cur, _) => ContactListPage(getIt.get(param1: cur))); + getIt.registerFactoryParam((CryptoCurrency? cur, _) => + ContactListPage(getIt.get(param1: cur), getIt.get())); getIt.registerFactoryParam( (ContactRecord? contact, _) => ContactPage(getIt.get(param1: contact))); @@ -702,13 +711,13 @@ Future setup({ )); getIt.registerFactory(() => ExchangeViewModel( - getIt.get().wallet!, - _tradesSource, - getIt.get(), - getIt.get(), - getIt.get().settingsStore, - getIt.get(), - )); + getIt.get().wallet!, + _tradesSource, + getIt.get(), + getIt.get(), + getIt.get().settingsStore, + getIt.get(), + getIt.get())); getIt.registerFactory(() => ExchangeTradeViewModel( wallet: getIt.get().wallet!, @@ -716,7 +725,8 @@ Future setup({ tradesStore: getIt.get(), sendViewModel: getIt.get())); - getIt.registerFactory(() => ExchangePage(getIt.get())); + getIt.registerFactory( + () => ExchangePage(getIt.get(), getIt.get())); getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get())); @@ -890,7 +900,7 @@ Future setup({ getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get())); - getIt.registerFactory(()=> MarketPlaceViewModel(getIt.get())); + getIt.registerFactory(() => MarketPlaceViewModel(getIt.get())); getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get())); diff --git a/lib/entities/cake_2fa_preset_options.dart b/lib/entities/cake_2fa_preset_options.dart new file mode 100644 index 000000000..2aa6c4215 --- /dev/null +++ b/lib/entities/cake_2fa_preset_options.dart @@ -0,0 +1,35 @@ +import 'package:cw_core/enumerable_item.dart'; + +class Cake2FAPresetsOptions extends EnumerableItem with Serializable { + const Cake2FAPresetsOptions({required String super.title, required int super.raw}); + + static const narrow = Cake2FAPresetsOptions(title: 'Narrow', raw: 0); + static const normal = Cake2FAPresetsOptions(title: 'Normal', raw: 1); + static const aggressive = Cake2FAPresetsOptions(title: 'Aggressive', raw: 2); + + static Cake2FAPresetsOptions deserialize({required int raw}) { + switch (raw) { + case 0: + return Cake2FAPresetsOptions.narrow; + case 1: + return Cake2FAPresetsOptions.normal; + case 2: + return Cake2FAPresetsOptions.aggressive; + default: + throw Exception( + 'Incorrect Cake 2FA Preset $raw for Cake2FAPresetOptions deserialize', + ); + } + } +} + +enum VerboseControlSettings { + accessWallet, + addingContacts, + sendsToContacts, + sendsToNonContacts, + sendsToInternalWallets, + exchangesToInternalWallets, + securityAndBackupSettings, + creatingNewWallets, +} diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 3bbaf4941..027e05f55 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -39,15 +39,31 @@ class PreferencesKey { static const lastPopupDate = 'last_popup_date'; static const lastAppReviewDate = 'last_app_review_date'; - - - static String moneroWalletUpdateV1Key(String name) - => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; + static String moneroWalletUpdateV1Key(String name) => + '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; static const exchangeProvidersSelection = 'exchange-providers-selection'; - static const clearnetDonationLink = 'clearnet_donation_link'; + static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const lastSeenAppVersion = 'last_seen_app_version'; - static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; + static const shouldShowMarketPlaceInDashboard = + 'should_show_marketplace_in_dashboard'; static const isNewInstall = 'is_new_install'; + static const shouldRequireTOTP2FAForAccessingWallet = + 'should_require_totp_2fa_for_accessing_wallets'; + static const shouldRequireTOTP2FAForSendsToContact = + 'should_require_totp_2fa_for_sends_to_contact'; + static const shouldRequireTOTP2FAForSendsToNonContact = + 'should_require_totp_2fa_for_sends_to_non_contact'; + static const shouldRequireTOTP2FAForSendsToInternalWallets = + 'should_require_totp_2fa_for_sends_to_internal_wallets'; + static const shouldRequireTOTP2FAForExchangesToInternalWallets = + 'should_require_totp_2fa_for_exchanges_to_internal_wallets'; + static const shouldRequireTOTP2FAForAddingContacts = + 'should_require_totp_2fa_for_adding_contacts'; + static const shouldRequireTOTP2FAForCreatingNewWallets = + 'should_require_totp_2fa_for_creating_new_wallets'; + static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + 'should_require_totp_2fa_for_all_security_and_backup_settings'; + static const selectedCake2FAPreset = 'selected_cake_2fa_preset'; } diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index ec8620ac6..601133c56 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/utils/show_bar.dart'; @@ -15,9 +16,10 @@ import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart import 'package:cake_wallet/src/widgets/collapsible_standart_list.dart'; class ContactListPage extends BasePage { - ContactListPage(this.contactListViewModel); + ContactListPage(this.contactListViewModel, this.authService); final ContactListViewModel contactListViewModel; + final AuthService authService; @override String get title => S.current.address_book; @@ -26,95 +28,99 @@ class ContactListPage extends BasePage { Widget? trailing(BuildContext context) { return MergeSemantics( child: Container( - width: 32.0, - height: 32.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context) - .accentTextTheme! - .bodySmall! - .color!), - child: Stack( - alignment: Alignment.center, - children: [ - Icon(Icons.add, - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!, - size: 22.0), - ButtonTheme( - minWidth: 32.0, - height: 32.0, - child: Semantics( - label: S.of(context).add, - child: TextButton( - // FIX-ME: Style - //shape: CircleBorder(), - onPressed: () async { - await Navigator.of(context) - .pushNamed(Routes.addressBookAddContact); - }, - child: Offstage()), - ), - ) - ], - )), + width: 32.0, + height: 32.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).accentTextTheme!.bodySmall!.color!), + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.add, + color: Theme.of(context).primaryTextTheme.titleLarge!.color!, + size: 22.0, + ), + ButtonTheme( + minWidth: 32.0, + height: 32.0, + child: TextButton( + // FIX-ME: Style + //shape: CircleBorder(), + onPressed: () async { + if (contactListViewModel + .shouldRequireTOTP2FAForAddingContacts) { + authService.authenticateAction( + context, + route: Routes.addressBookAddContact, + conditionToDetermineIfToUse2FA: contactListViewModel + .shouldRequireTOTP2FAForAddingContacts, + ); + } else { + await Navigator.of(context) + .pushNamed(Routes.addressBookAddContact); + } + }, + child: Offstage()), + ) + ], + ), + ), ); } @override Widget body(BuildContext context) { - return Container( padding: EdgeInsets.only(top: 20.0, bottom: 20.0), - child: Observer( - builder: (_) { + child: Observer(builder: (_) { final contacts = contactListViewModel.contactsToShow; final walletContacts = contactListViewModel.walletContactsToShow; return CollapsibleSectionList( - context: context, - sectionCount: 2, - themeColor: Theme.of(context).primaryTextTheme!.titleLarge!.color!, - dividerThemeColor: - Theme.of(context).primaryTextTheme!.bodySmall!.decorationColor!, - sectionTitleBuilder: (_, int sectionIndex) { - var title = S.current.contact_list_contacts; + context: context, + sectionCount: 2, + themeColor: Theme.of(context).primaryTextTheme.titleLarge!.color!, + dividerThemeColor: + Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!, + sectionTitleBuilder: (_, int sectionIndex) { + var title = S.current.contact_list_contacts; - if (sectionIndex == 0) { - title = S.current.contact_list_wallets; - } + if (sectionIndex == 0) { + title = S.current.contact_list_wallets; + } - return Container( - padding: EdgeInsets.only(bottom: 10), - child: Text(title, style: TextStyle(fontSize: 36))); - }, - itemCounter: (int sectionIndex) => sectionIndex == 0 - ? walletContacts.length - : contacts.length, - itemBuilder: (_, sectionIndex, index) { - if (sectionIndex == 0) { - final walletInfo = walletContacts[index]; - return generateRaw(context, walletInfo); - } + return Container( + padding: EdgeInsets.only(bottom: 10), + child: Text(title, style: TextStyle(fontSize: 36))); + }, + itemCounter: (int sectionIndex) => + sectionIndex == 0 ? walletContacts.length : contacts.length, + itemBuilder: (_, sectionIndex, index) { + if (sectionIndex == 0) { + final walletInfo = walletContacts[index]; + return generateRaw(context, walletInfo); + } - final contact = contacts[index]; - final content = generateRaw(context, contact); - return contactListViewModel.isEditable - ? Slidable( - key: Key('${contact.key}'), - endActionPane: _actionPane(context, contact), - child: content, - ) - : content; - }, - );}) - ); + final contact = contacts[index]; + final content = generateRaw(context, contact); + return contactListViewModel.isEditable + ? Slidable( + key: Key('${contact.key}'), + endActionPane: _actionPane(context, contact), + child: content, + ) + : content; + }, + ); + })); } Widget generateRaw(BuildContext context, ContactBase contact) { final image = contact.type.iconPath; - final currencyIcon = image != null ? Image.asset(image, height: 24, width: 24) + final currencyIcon = image != null + ? Image.asset(image, height: 24, width: 24) : const SizedBox(height: 24, width: 24); - return GestureDetector( onTap: () async { if (!contactListViewModel.isEditable) { @@ -128,30 +134,28 @@ class ContactListPage extends BasePage { if (isCopied) { await Clipboard.setData(ClipboardData(text: contact.address)); await showBar(context, S.of(context).copied_to_clipboard); - } }, child: Container( color: Colors.transparent, - padding: - const EdgeInsets.only(top: 16, bottom: 16, right: 24), + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ currencyIcon, Expanded( - child: Padding( - padding: EdgeInsets.only(left: 12), - child: Text( - contact.name, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!), + child: Padding( + padding: EdgeInsets.only(left: 12), + child: Text( + contact.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).primaryTextTheme.titleLarge!.color!, ), - ) - ) + ), + )) ], ), ), @@ -160,60 +164,61 @@ class ContactListPage extends BasePage { Future showAlertDialog(BuildContext context) async { return await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).address_remove_contact, - alertContent: S.of(context).address_remove_content, - rightButtonText: S.of(context).remove, - leftButtonText: S.of(context).cancel, - actionRightButton: () => Navigator.of(context).pop(true), - actionLeftButton: () => Navigator.of(context).pop(false)); - }) ?? false; + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).address_remove_contact, + alertContent: S.of(context).address_remove_content, + rightButtonText: S.of(context).remove, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; } Future showNameAndAddressDialog( BuildContext context, String name, String address) async { return await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: name, - alertContent: address, - rightButtonText: S.of(context).copy, - leftButtonText: S.of(context).cancel, - actionRightButton: () => Navigator.of(context).pop(true), - actionLeftButton: () => Navigator.of(context).pop(false)); - }) ?? false; + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: name, + alertContent: address, + rightButtonText: S.of(context).copy, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; } - ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.4, - children: [ - SlidableAction( - onPressed: (_) async => await Navigator.of(context) - .pushNamed(Routes.addressBookAddContact, - arguments: contact), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - icon: Icons.edit, - label: S.of(context).edit, - ), - SlidableAction( - onPressed: (_) async { - final isDelete = - await showAlertDialog(context); + ActionPane _actionPane(BuildContext context, ContactRecord contact) => + ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.4, + children: [ + SlidableAction( + onPressed: (_) async => await Navigator.of(context) + .pushNamed(Routes.addressBookAddContact, arguments: contact), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: Icons.edit, + label: S.of(context).edit, + ), + SlidableAction( + onPressed: (_) async { + final isDelete = await showAlertDialog(context); - if (isDelete) { - await contactListViewModel.delete(contact); - } - }, - backgroundColor: Colors.red, - foregroundColor: Colors.white, - icon: CupertinoIcons.delete, - label: S.of(context).delete, - ), - ], - ); + if (isDelete) { + await contactListViewModel.delete(contact); + } + }, + backgroundColor: Colors.red, + foregroundColor: Colors.white, + icon: CupertinoIcons.delete, + label: S.of(context).delete, + ), + ], + ); } 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 501252978..6076d418f 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 @@ -156,15 +156,29 @@ class _DesktopWalletSelectionDropDownState extends State(); final receiveKey = GlobalKey(); final _formKey = GlobalKey(); @@ -89,16 +91,17 @@ class ExchangePage extends BasePage { @override Widget middle(BuildContext context) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(right:6.0), - child: Observer(builder: (_) => SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus),) - ), - PresentProviderPicker(exchangeViewModel: exchangeViewModel) - ], - ); - + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(right: 6.0), + child: Observer( + builder: (_) => + SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus), + )), + PresentProviderPicker(exchangeViewModel: exchangeViewModel) + ], + ); @override Widget trailing(BuildContext context) => TrailButton( @@ -110,12 +113,13 @@ class ExchangePage extends BasePage { @override Widget? leading(BuildContext context) { - final _backButton = Icon(Icons.arrow_back_ios, + final _backButton = Icon( + Icons.arrow_back_ios, color: titleColor, size: 16, ); - final _closeButton = currentTheme.type == ThemeType.dark - ? closeButtonImageDarkTheme : closeButtonImage; + final _closeButton = + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; bool isMobileView = ResponsiveLayoutUtil.instance.isMobile; @@ -126,13 +130,10 @@ class ExchangePage extends BasePage { child: ButtonTheme( minWidth: double.minPositive, child: Semantics( - label: !isMobileView - ? S.of(context).close - : S.of(context).seed_alert_back, + label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back, child: TextButton( style: ButtonStyle( - overlayColor: MaterialStateColor.resolveWith( - (states) => Colors.transparent), + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), ), onPressed: () => onClose(context), child: !isMobileView ? _closeButton : _backButton, @@ -145,23 +146,19 @@ class ExchangePage extends BasePage { @override Widget body(BuildContext context) { - WidgetsBinding.instance - .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); + WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); return KeyboardActions( disableScroll: true, config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: - Theme.of(context).accentTextTheme!.bodyLarge!.backgroundColor!, + keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!, nextFocus: false, actions: [ KeyboardActionsItem( - focusNode: _depositAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()]), + focusNode: _depositAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]), KeyboardActionsItem( - focusNode: _receiveAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()]) + focusNode: _receiveAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]) ]), child: Container( color: Theme.of(context).colorScheme.background, @@ -169,30 +166,28 @@ class ExchangePage extends BasePage { key: _formKey, child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(bottom: 24), - content: Observer(builder: (_) => Column( - children: [ - _exchangeCardsSection(context), - Padding( - padding: EdgeInsets.only(top: 12, left: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - StandardCheckbox( - value: exchangeViewModel.isFixedRateMode, - caption: S.of(context).fixed_rate, - onChanged: (value) => - exchangeViewModel.isFixedRateMode = value, - ), - ], - ) - ), - SizedBox(height: 30), - _buildTemplateSection(context) + content: Observer( + builder: (_) => Column( + children: [ + _exchangeCardsSection(context), + Padding( + padding: EdgeInsets.only(top: 12, left: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + StandardCheckbox( + value: exchangeViewModel.isFixedRateMode, + caption: S.of(context).fixed_rate, + onChanged: (value) => exchangeViewModel.isFixedRateMode = value, + ), + ], + )), + SizedBox(height: 30), + _buildTemplateSection(context) ], ), ), - bottomSectionPadding: - EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column(children: [ Padding( padding: EdgeInsets.only(bottom: 15), @@ -210,8 +205,7 @@ class ExchangePage extends BasePage { textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context) - .primaryTextTheme! - .displayLarge! + .primaryTextTheme.displayLarge! .decorationColor!, fontWeight: FontWeight.w500, fontSize: 12), @@ -223,29 +217,34 @@ class ExchangePage extends BasePage { builder: (_) => LoadingPrimaryButton( text: S.of(context).exchange, onPressed: () { - if (_formKey.currentState != null && _formKey.currentState!.validate()) { - if ((exchangeViewModel.depositCurrency == - CryptoCurrency.xmr) && - (!(exchangeViewModel.status - is SyncedSyncStatus))) { + if (_formKey.currentState != null && + _formKey.currentState!.validate()) { + if ((exchangeViewModel.depositCurrency == CryptoCurrency.xmr) && + (!(exchangeViewModel.status is SyncedSyncStatus))) { showPopUp( context: context, builder: (BuildContext context) { return AlertWithOneAction( alertTitle: S.of(context).exchange, - alertContent: S - .of(context) - .exchange_sync_alert_content, + alertContent: S.of(context).exchange_sync_alert_content, buttonText: S.of(context).ok, - buttonAction: () => - Navigator.of(context).pop()); + buttonAction: () => Navigator.of(context).pop()); }); } else { - exchangeViewModel.createTrade(); + final check = exchangeViewModel.shouldDisplayTOTP(); + authService.authenticateAction( + context, + conditionToDetermineIfToUse2FA: check, + onAuthSuccess: (value) { + if (value) { + exchangeViewModel.createTrade(); + } + }, + ); } } }, - color: Theme.of(context).accentTextTheme!.bodyLarge!.color!, + color: Theme.of(context).accentTextTheme.bodyLarge!.color!, textColor: Colors.white, isDisabled: exchangeViewModel.selectedProviders.isEmpty, isLoading: exchangeViewModel.tradeState is TradeIsCreating)), @@ -264,7 +263,7 @@ class ExchangePage extends BasePage { child: Observer( builder: (_) { final templates = exchangeViewModel.templates; - + return Row( children: [ AddTemplateButton( @@ -293,18 +292,15 @@ class ExchangePage extends BasePage { builder: (dialogContext) { return AlertWithTwoActions( alertTitle: S.of(context).template, - alertContent: - S.of(context).confirm_delete_template, + alertContent: S.of(context).confirm_delete_template, rightButtonText: S.of(context).delete, leftButtonText: S.of(context).cancel, actionRightButton: () { Navigator.of(dialogContext).pop(); - exchangeViewModel.removeTemplate( - template: template); + exchangeViewModel.removeTemplate(template: template); exchangeViewModel.updateTemplate(); }, - actionLeftButton: () => - Navigator.of(dialogContext).pop()); + actionLeftButton: () => Navigator.of(dialogContext).pop()); }); }, ); @@ -318,8 +314,8 @@ class ExchangePage extends BasePage { ); } - void applyTemplate(BuildContext context, - ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async { + void applyTemplate( + BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async { exchangeViewModel.changeDepositCurrency( currency: CryptoCurrency.fromString(template.depositCurrency)); exchangeViewModel.changeReceiveCurrency( @@ -333,22 +329,19 @@ class ExchangePage extends BasePage { var domain = template.depositAddress; var ticker = template.depositCurrency.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, ticker); + exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, ticker); domain = template.receiveAddress; ticker = template.receiveCurrency.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, ticker); + exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, ticker); } - void _setReactions( - BuildContext context, ExchangeViewModel exchangeViewModel) { + void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) { if (_isReactionsSet) { return; } - if (exchangeViewModel.isLowFee) { + if (exchangeViewModel.isLowFee) { _showFeeAlert(context); } @@ -359,42 +352,30 @@ class ExchangePage extends BasePage { final limitsState = exchangeViewModel.limitsState; if (limitsState is LimitsLoadedSuccessfully) { - final min = limitsState.limits.min != null - ? limitsState.limits.min.toString() - : null; - final max = limitsState.limits.max != null - ? limitsState.limits.max.toString() - : null; - final key = exchangeViewModel.isFixedRateMode - ? receiveKey - : depositKey; + final min = limitsState.limits.min != null ? limitsState.limits.min.toString() : null; + final max = limitsState.limits.max != null ? limitsState.limits.max.toString() : null; + final key = exchangeViewModel.isFixedRateMode ? receiveKey : depositKey; key.currentState!.changeLimits(min: min, max: max); } - _onCurrencyChange( - exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey); - _onCurrencyChange( - exchangeViewModel.depositCurrency, exchangeViewModel, depositKey); + _onCurrencyChange(exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey); + _onCurrencyChange(exchangeViewModel.depositCurrency, exchangeViewModel, depositKey); reaction( (_) => exchangeViewModel.wallet.name, - (String _) => _onWalletNameChange( - exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey)); + (String _) => + _onWalletNameChange(exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey)); reaction( (_) => exchangeViewModel.wallet.name, - (String _) => _onWalletNameChange( - exchangeViewModel, exchangeViewModel.depositCurrency, depositKey)); + (String _) => + _onWalletNameChange(exchangeViewModel, exchangeViewModel.depositCurrency, depositKey)); - reaction( - (_) => exchangeViewModel.receiveCurrency, - (CryptoCurrency currency) => - _onCurrencyChange(currency, exchangeViewModel, receiveKey)); + reaction((_) => exchangeViewModel.receiveCurrency, + (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, receiveKey)); - reaction( - (_) => exchangeViewModel.depositCurrency, - (CryptoCurrency currency) => - _onCurrencyChange(currency, exchangeViewModel, depositKey)); + reaction((_) => exchangeViewModel.depositCurrency, + (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey)); reaction((_) => exchangeViewModel.depositAmount, (String amount) { if (depositKey.currentState!.amountController.text != amount) { @@ -408,8 +389,7 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.isDepositAddressEnabled, - (bool isEnabled) { + reaction((_) => exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) { depositKey.currentState!.isAddressEditable(isEditable: isEnabled); }); @@ -425,13 +405,11 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.isReceiveAddressEnabled, - (bool isEnabled) { + reaction((_) => exchangeViewModel.isReceiveAddressEnabled, (bool isEnabled) { receiveKey.currentState!.isAddressEditable(isEditable: isEnabled); }); - reaction((_) => exchangeViewModel.isReceiveAmountEditable, - (bool isReceiveAmountEditable) { + reaction((_) => exchangeViewModel.isReceiveAmountEditable, (bool isReceiveAmountEditable) { receiveKey.currentState!.isAmountEditable(isEditable: isReceiveAmountEditable); }); @@ -483,20 +461,20 @@ class ExchangePage extends BasePage { } }); - depositAddressController.addListener( - () => exchangeViewModel.depositAddress = depositAddressController.text); + depositAddressController + .addListener(() => exchangeViewModel.depositAddress = depositAddressController.text); depositAmountController.addListener(() { if (depositAmountController.text != exchangeViewModel.depositAmount) { - _depositAmountDebounce.run(() { + _depositAmountDebounce.run(() { exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); exchangeViewModel.isReceiveAmountEntered = false; }); } }); - receiveAddressController.addListener( - () => exchangeViewModel.receiveAddress = receiveAddressController.text); + receiveAddressController + .addListener(() => exchangeViewModel.receiveAddress = receiveAddressController.text); receiveAmountController.addListener(() { if (receiveAmountController.text != exchangeViewModel.receiveAmount) { @@ -507,8 +485,7 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.wallet.walletAddresses.address, - (String address) { + reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) { if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) { depositKey.currentState!.changeAddress(address: address); } @@ -519,22 +496,18 @@ class ExchangePage extends BasePage { }); _depositAddressFocus.addListener(() async { - if (!_depositAddressFocus.hasFocus && - depositAddressController.text.isNotEmpty) { + if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) { final domain = depositAddressController.text; final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, ticker); + exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, ticker); } }); _receiveAddressFocus.addListener(() async { - if (!_receiveAddressFocus.hasFocus && - receiveAddressController.text.isNotEmpty) { + if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) { final domain = receiveAddressController.text; final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, ticker); + exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, ticker); } }); @@ -554,29 +527,26 @@ class ExchangePage extends BasePage { _isReactionsSet = true; } - void _onCurrencyChange(CryptoCurrency currency, - ExchangeViewModel exchangeViewModel, GlobalKey key) { + void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel, + GlobalKey key) { final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency; key.currentState!.changeSelectedCurrency(currency); - key.currentState!.changeWalletName( - isCurrentTypeWallet ? exchangeViewModel.wallet.name : ''); + key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : ''); key.currentState!.changeAddress( - address: isCurrentTypeWallet - ? exchangeViewModel.wallet.walletAddresses.address : ''); + address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : ''); key.currentState!.changeAmount(amount: ''); } - void _onWalletNameChange(ExchangeViewModel exchangeViewModel, - CryptoCurrency currency, GlobalKey key) { + void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency, + GlobalKey key) { final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency; if (isCurrentTypeWallet) { key.currentState!.changeWalletName(exchangeViewModel.wallet.name); - key.currentState!.addressController.text = - exchangeViewModel.wallet.walletAddresses.address; + key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address; } else if (key.currentState!.addressController.text == exchangeViewModel.wallet.walletAddresses.address) { key.currentState!.changeWalletName(''); @@ -584,8 +554,7 @@ class ExchangePage extends BasePage { } } - Future fetchParsedAddress( - BuildContext context, String domain, String ticker) async { + Future fetchParsedAddress(BuildContext context, String domain, String ticker) async { final parsedAddress = await getIt.get().resolve(domain, ticker); final address = await extractAddressFromParsed(context, parsedAddress); return address; @@ -594,16 +563,17 @@ class ExchangePage extends BasePage { void _showFeeAlert(BuildContext context) async { await Future.delayed(Duration(seconds: 1)); final confirmed = await showPopUp( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).low_fee, - alertContent: S.of(context).low_fee_alert, - leftButtonText: S.of(context).ignor, - rightButtonText: S.of(context).use_suggested, - actionLeftButton: () => Navigator.of(dialogContext).pop(false), - actionRightButton: () => Navigator.of(dialogContext).pop(true)); - }) ?? false; + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).low_fee, + alertContent: S.of(context).low_fee_alert, + leftButtonText: S.of(context).ignor, + rightButtonText: S.of(context).use_suggested, + actionLeftButton: () => Navigator.of(dialogContext).pop(false), + actionRightButton: () => Navigator.of(dialogContext).pop(true)); + }) ?? + false; if (confirmed) { exchangeViewModel.setDefaultTransactionPriority(); } @@ -612,126 +582,122 @@ class ExchangePage extends BasePage { void disposeBestRateSync() => exchangeViewModel.bestRateSync.cancel(); Widget _exchangeCardsSection(BuildContext context) { - final firstExchangeCard = Observer(builder: (_) => ExchangeCard( - onDispose: disposeBestRateSync, - hasAllAmount: exchangeViewModel.hasAllAmount, - allAmount: exchangeViewModel.hasAllAmount - ? () => exchangeViewModel.calculateDepositAllAmount() - : null, - amountFocusNode: _depositAmountFocus, - addressFocusNode: _depositAddressFocus, - key: depositKey, - title: S.of(context).you_will_send, - initialCurrency: exchangeViewModel.depositCurrency, - initialWalletName: depositWalletName ?? '', - initialAddress: - exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address - : exchangeViewModel.depositAddress, - initialIsAmountEditable: true, - initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, - isAmountEstimated: false, - hasRefundAddress: true, - isMoneroWallet: exchangeViewModel.isMoneroWallet, - currencies: exchangeViewModel.depositCurrencies, - onCurrencySelected: (currency) { - // FIXME: need to move it into view model - if (currency == CryptoCurrency.xmr && - exchangeViewModel.wallet.type != WalletType.monero) { - showPopUp( - context: context, - builder: (dialogContext) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: - S.of(context).exchange_incorrect_current_wallet_for_xmr, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(dialogContext).pop()); - }); - return; - } + final firstExchangeCard = Observer( + builder: (_) => ExchangeCard( + onDispose: disposeBestRateSync, + hasAllAmount: exchangeViewModel.hasAllAmount, + allAmount: exchangeViewModel.hasAllAmount + ? () => exchangeViewModel.calculateDepositAllAmount() + : null, + amountFocusNode: _depositAmountFocus, + addressFocusNode: _depositAddressFocus, + key: depositKey, + title: S.of(context).you_will_send, + initialCurrency: exchangeViewModel.depositCurrency, + initialWalletName: depositWalletName ?? '', + initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency + ? exchangeViewModel.wallet.walletAddresses.address + : exchangeViewModel.depositAddress, + initialIsAmountEditable: true, + initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, + isAmountEstimated: false, + hasRefundAddress: true, + isMoneroWallet: exchangeViewModel.isMoneroWallet, + currencies: exchangeViewModel.depositCurrencies, + onCurrencySelected: (currency) { + // FIXME: need to move it into view model + if (currency == CryptoCurrency.xmr && + exchangeViewModel.wallet.type != WalletType.monero) { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: S.of(context).exchange_incorrect_current_wallet_for_xmr, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(dialogContext).pop()); + }); + return; + } - exchangeViewModel.changeDepositCurrency(currency: currency); - }, - imageArrow: arrowBottomPurple, - currencyButtonColor: Colors.transparent, - addressButtonsColor: Theme.of(context).focusColor!, - borderColor: Theme.of(context).primaryTextTheme!.bodyLarge!.color!, - currencyValueValidator: (value) { - return !exchangeViewModel.isFixedRateMode - ? AmountValidator( - isAutovalidate: true, - currency: exchangeViewModel.depositCurrency, - minValue: exchangeViewModel.limits.min.toString(), - maxValue: exchangeViewModel.limits.max.toString(), - ).call(value) - : null; - }, - addressTextFieldValidator: - AddressValidator(type: exchangeViewModel.depositCurrency), - onPushPasteButton: (context) async { - final domain = exchangeViewModel.depositAddress; - final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, ticker); - }, - onPushAddressBookButton: (context) async { - final domain = exchangeViewModel.depositAddress; - final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, ticker); - }, - )); + exchangeViewModel.changeDepositCurrency(currency: currency); + }, + imageArrow: arrowBottomPurple, + currencyButtonColor: Colors.transparent, + addressButtonsColor: Theme.of(context).focusColor, + borderColor: Theme.of(context).primaryTextTheme.bodyLarge!.color!, + currencyValueValidator: (value) { + return !exchangeViewModel.isFixedRateMode + ? AmountValidator( + isAutovalidate: true, + currency: exchangeViewModel.depositCurrency, + minValue: exchangeViewModel.limits.min.toString(), + maxValue: exchangeViewModel.limits.max.toString(), + ).call(value) + : null; + }, + addressTextFieldValidator: AddressValidator(type: exchangeViewModel.depositCurrency), + onPushPasteButton: (context) async { + final domain = exchangeViewModel.depositAddress; + final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); + exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, ticker); + }, + onPushAddressBookButton: (context) async { + final domain = exchangeViewModel.depositAddress; + final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); + exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, ticker); + }, + )); - final secondExchangeCard = Observer(builder: (_) => ExchangeCard( - onDispose: disposeBestRateSync, - amountFocusNode: _receiveAmountFocus, - addressFocusNode: _receiveAddressFocus, - key: receiveKey, - title: S.of(context).you_will_get, - initialCurrency: exchangeViewModel.receiveCurrency, - initialWalletName: receiveWalletName ?? '', - initialAddress: - exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address - : exchangeViewModel.receiveAddress, - initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable, - initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled, - isAmountEstimated: true, - isMoneroWallet: exchangeViewModel.isMoneroWallet, - currencies: exchangeViewModel.receiveCurrencies, - onCurrencySelected: (currency) => - exchangeViewModel.changeReceiveCurrency(currency: currency), - imageArrow: arrowBottomCakeGreen, - currencyButtonColor: Colors.transparent, - addressButtonsColor: Theme.of(context).focusColor!, - borderColor: - Theme.of(context).primaryTextTheme!.bodyLarge!.decorationColor!, - currencyValueValidator: (value) { - return exchangeViewModel.isFixedRateMode - ? AmountValidator( - isAutovalidate: true, - currency: exchangeViewModel.receiveCurrency, - minValue: exchangeViewModel.limits.min.toString(), - maxValue: exchangeViewModel.limits.max.toString(), - ).call(value) - : null; - }, - addressTextFieldValidator: - AddressValidator(type: exchangeViewModel.receiveCurrency), - onPushPasteButton: (context) async { - final domain = exchangeViewModel.receiveAddress; - final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, ticker); - }, - onPushAddressBookButton: (context) async { - final domain = exchangeViewModel.receiveAddress; - final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, ticker); - }, - )); + final secondExchangeCard = Observer( + builder: (_) => ExchangeCard( + onDispose: disposeBestRateSync, + amountFocusNode: _receiveAmountFocus, + addressFocusNode: _receiveAddressFocus, + key: receiveKey, + title: S.of(context).you_will_get, + initialCurrency: exchangeViewModel.receiveCurrency, + initialWalletName: receiveWalletName ?? '', + initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency + ? exchangeViewModel.wallet.walletAddresses.address + : exchangeViewModel.receiveAddress, + initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable, + initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled, + isAmountEstimated: true, + isMoneroWallet: exchangeViewModel.isMoneroWallet, + currencies: exchangeViewModel.receiveCurrencies, + onCurrencySelected: (currency) => + exchangeViewModel.changeReceiveCurrency(currency: currency), + imageArrow: arrowBottomCakeGreen, + currencyButtonColor: Colors.transparent, + addressButtonsColor: Theme.of(context).focusColor, + borderColor: Theme.of(context).primaryTextTheme.bodyLarge!.decorationColor!, + currencyValueValidator: (value) { + return exchangeViewModel.isFixedRateMode + ? AmountValidator( + isAutovalidate: true, + currency: exchangeViewModel.receiveCurrency, + minValue: exchangeViewModel.limits.min.toString(), + maxValue: exchangeViewModel.limits.max.toString(), + ).call(value) + : null; + }, + addressTextFieldValidator: AddressValidator(type: exchangeViewModel.receiveCurrency), + onPushPasteButton: (context) async { + final domain = exchangeViewModel.receiveAddress; + final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); + exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, ticker); + }, + onPushAddressBookButton: (context) async { + final domain = exchangeViewModel.receiveAddress; + final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); + exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, ticker); + }, + )); if (ResponsiveLayoutUtil.instance.isMobile) { return MobileExchangeCardsSection( diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 3ce7e133a..3298a50c0 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -97,7 +97,8 @@ class RootState extends State with WidgetsBindingObserver { return; } - if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) { + if (!_isInactive && + widget.authenticationStore.state == AuthenticationState.allowed) { setState(() => _setInactive(true)); } @@ -124,13 +125,16 @@ class RootState extends State with WidgetsBindingObserver { return; } else { final useTotp = widget.appStore.settingsStore.useTOTP2FA; - if (useTotp) { + final shouldUseTotp2FAToAccessWallets = widget.appStore + .settingsStore.shouldRequireTOTP2FAForAccessingWallet; + if (useTotp && shouldUseTotp2FAToAccessWallets) { _reset(); auth.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( onTotpAuthenticationFinished: - (bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) { + (bool isAuthenticatedSuccessfully, + TotpAuthCodePageState totpAuth) { if (!isAuthenticatedSuccessfully) { return; } @@ -151,15 +155,11 @@ class RootState extends State with WidgetsBindingObserver { route: launchUri != null ? Routes.send : null, arguments: PaymentRequest.fromUri(launchUri), ); - launchUri = null; + launchUri = null; } } - - - }, - ); - - + }, + ); }); } else if (launchUri != null) { widget.navigatorKey.currentState?.pushNamed( diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 3b8bf6877..e363f7706 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; @@ -32,10 +33,12 @@ import 'package:cw_core/crypto_currency.dart'; class SendPage extends BasePage { SendPage({ required this.sendViewModel, + required this.authService, this.initialPaymentRequest, }) : _formKey = GlobalKey(); final SendViewModel sendViewModel; + final AuthService authService; final GlobalKey _formKey; final controller = PageController(initialPage: 0); final PaymentRequest? initialPaymentRequest; @@ -56,12 +59,14 @@ class SendPage extends BasePage { @override Widget? leading(BuildContext context) { - final _backButton = Icon(Icons.arrow_back_ios, + final _backButton = Icon( + Icons.arrow_back_ios, color: titleColor, size: 16, ); final _closeButton = currentTheme.type == ThemeType.dark - ? closeButtonImageDarkTheme : closeButtonImage; + ? closeButtonImageDarkTheme + : closeButtonImage; bool isMobileView = ResponsiveLayoutUtil.instance.isMobile; @@ -78,7 +83,7 @@ class SendPage extends BasePage { child: TextButton( style: ButtonStyle( overlayColor: MaterialStateColor.resolveWith( - (states) => Colors.transparent), + (states) => Colors.transparent), ), onPressed: () => onClose(context), child: !isMobileView ? _closeButton : _backButton, @@ -114,11 +119,13 @@ class SendPage extends BasePage { mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( - padding: const EdgeInsets.only(right:8.0), - child: Observer(builder: (_) => SyncIndicatorIcon(isSynced: sendViewModel.isReadyForSend),), + padding: const EdgeInsets.only(right: 8.0), + child: Observer( + builder: (_) => + SyncIndicatorIcon(isSynced: sendViewModel.isReadyForSend), + ), ), - if (supMiddle != null) - supMiddle + if (supMiddle != null) supMiddle ], ); } @@ -200,12 +207,12 @@ class SendPage extends BasePage { dotWidth: 6.0, dotHeight: 6.0, dotColor: Theme.of(context) - .primaryTextTheme - !.displaySmall! + .primaryTextTheme! + .displaySmall! .backgroundColor!, activeDotColor: Theme.of(context) - .primaryTextTheme - !.displayMedium! + .primaryTextTheme! + .displayMedium! .backgroundColor!), ) : Offstage(); @@ -339,8 +346,8 @@ class SendPage extends BasePage { 'Change your asset (${sendViewModel.selectedCryptoCurrency})', color: Colors.transparent, textColor: Theme.of(context) - .accentTextTheme - !.displaySmall! + .accentTextTheme! + .displaySmall! .decorationColor!, ))), if (sendViewModel.hasMultiRecipient) @@ -357,13 +364,13 @@ class SendPage extends BasePage { text: S.of(context).add_receiver, color: Colors.transparent, textColor: Theme.of(context) - .accentTextTheme - !.displaySmall! + .accentTextTheme! + .displaySmall! .decorationColor!, isDottedBorder: true, borderColor: Theme.of(context) - .primaryTextTheme - !.displaySmall! + .primaryTextTheme! + .displaySmall! .decorationColor!, )), Observer( @@ -390,7 +397,16 @@ class SendPage extends BasePage { return; } - await sendViewModel.createTransaction(); + final check = sendViewModel.shouldDisplayTotp(); + authService.authenticateAction( + context, + conditionToDetermineIfToUse2FA: check, + onAuthSuccess: (value) async { + if (value) { + await sendViewModel.createTransaction(); + } + }, + ); }, text: S.of(context).send, color: diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index 6b72a480a..a0fb16cb6 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -30,12 +30,22 @@ class SecurityBackupPage extends BasePage { child: Column(mainAxisSize: MainAxisSize.min, children: [ SettingsCellWithArrow( title: S.current.show_keys, - handler: (_) => _authService.authenticateAction(context, route: Routes.showKeys), + handler: (_) => _authService.authenticateAction( + context, + route: Routes.showKeys, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), SettingsCellWithArrow( title: S.current.create_backup, - handler: (_) => _authService.authenticateAction(context, route: Routes.backup), + handler: (_) => _authService.authenticateAction( + context, + route: Routes.backup, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), SettingsCellWithArrow( @@ -46,6 +56,8 @@ class SecurityBackupPage extends BasePage { arguments: (PinCodeState setupPinContext, String _) { setupPinContext.close(); }, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), @@ -67,7 +79,10 @@ class SecurityBackupPage extends BasePage { _securitySettingsViewModel .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); } - }); + }, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ); } else { _securitySettingsViewModel.setAllowBiometricalAuthentication(value); } @@ -94,6 +109,8 @@ class SecurityBackupPage extends BasePage { route: _securitySettingsViewModel.useTotp2FA ? Routes.modify2FAPage : Routes.setup_2faPage, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), ); }, diff --git a/lib/src/screens/settings/widgets/settings_choices_cell.dart b/lib/src/screens/settings/widgets/settings_choices_cell.dart index 49bc301f1..c03c923c3 100644 --- a/lib/src/screens/settings/widgets/settings_choices_cell.dart +++ b/lib/src/screens/settings/widgets/settings_choices_cell.dart @@ -22,7 +22,7 @@ class SettingsChoicesCell extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!, + color: Theme.of(context).primaryTextTheme.titleLarge!.color!, ), ), ], @@ -34,10 +34,7 @@ class SettingsChoicesCell extends StatelessWidget { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), - color: Theme.of(context) - .accentTextTheme! - .displaySmall! - .color!, + color: Theme.of(context).accentTextTheme.displaySmall!.color!, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -52,10 +49,7 @@ class SettingsChoicesCell extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), color: isSelected - ? Theme.of(context) - .accentTextTheme! - .bodyLarge! - .color! + ? Theme.of(context).accentTextTheme.bodyLarge!.color! : null, ), child: Text( @@ -63,10 +57,7 @@ class SettingsChoicesCell extends StatelessWidget { style: TextStyle( color: isSelected ? Colors.white - : Theme.of(context) - .primaryTextTheme! - .bodySmall! - .color!, + : Theme.of(context).primaryTextTheme.bodySmall!.color!, fontWeight: isSelected ? FontWeight.w700 : FontWeight.normal, ), ), diff --git a/lib/src/screens/setup_2fa/modify_2fa_page.dart b/lib/src/screens/setup_2fa/modify_2fa_page.dart index 05c06fe3e..148e3076e 100644 --- a/lib/src/screens/setup_2fa/modify_2fa_page.dart +++ b/lib/src/screens/setup_2fa/modify_2fa_page.dart @@ -1,12 +1,16 @@ +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/generated/i18n.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/show_pop_up.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; import 'package:flutter/material.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/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import '../../../routes.dart'; @@ -21,35 +25,148 @@ class Modify2FAPage extends BasePage { @override Widget body(BuildContext context) { return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsCellWithArrow( - title: S.current.disable_cake_2fa, - handler: (_) async { - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.current.disable_cake_2fa, - alertContent: S.current.question_to_disable_2fa, - leftButtonText: S.current.cancel, - rightButtonText: S.current.disable, - actionLeftButton: () { - Navigator.of(context).pop(); - }, - actionRightButton: () { - setup2FAViewModel.setUseTOTP2FA(false); - Navigator.pushNamedAndRemoveUntil( - context, Routes.dashboard, (route) => false); - }, - ); - }, - ); - }), - StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), - ], - ), + child: _2FAControlsWidget(setup2FAViewModel: setup2FAViewModel), + ); + } +} + +class _2FAControlsWidget extends StatelessWidget { + const _2FAControlsWidget({required this.setup2FAViewModel}); + + final Setup2FAViewModel setup2FAViewModel; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsCellWithArrow( + title: S.current.disable_cake_2fa, + handler: (_) async { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.current.disable_cake_2fa, + alertContent: S.current.question_to_disable_2fa, + leftButtonText: S.current.cancel, + rightButtonText: S.current.disable, + actionLeftButton: () => Navigator.of(context).pop(), + actionRightButton: () { + setup2FAViewModel.setUseTOTP2FA(false); + Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false); + }, + ); + }, + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsChoicesCell( + ChoicesListItem( + title: S.current.cake_2fa_preset, + onItemSelected: setup2FAViewModel.selectCakePreset, + selectedItem: setup2FAViewModel.selectedCake2FAPreset, + items: [ + Cake2FAPresetsOptions.narrow, + Cake2FAPresetsOptions.normal, + Cake2FAPresetsOptions.aggressive, + ], + ), + ); + }, + ), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_assessing_wallet, + value: setup2FAViewModel.shouldRequireTOTP2FAForAccessingWallet, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForAccessingWallet(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_sends_to_non_contacts, + value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToNonContact, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToNonContact(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_sends_to_contacts, + value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToContact, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToContact(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_sends_to_internal_wallets, + value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToInternalWallets, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToInternalWallets(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_exchanges_to_internal_wallets, + value: setup2FAViewModel.shouldRequireTOTP2FAForExchangesToInternalWallets, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForExchangesToInternalWallets(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_adding_contacts, + value: setup2FAViewModel.shouldRequireTOTP2FAForAddingContacts, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForAddingContacts(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_creating_new_wallets, + value: setup2FAViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForCreatingNewWallet(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_all_security_and_backup_settings, + value: setup2FAViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + onValueChange: (context, value) async => setup2FAViewModel + .switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + ], ); } } diff --git a/lib/src/screens/setup_2fa/setup_2fa.dart b/lib/src/screens/setup_2fa/setup_2fa.dart index 5da0fb120..6c60dcd1e 100644 --- a/lib/src/screens/setup_2fa/setup_2fa.dart +++ b/lib/src/screens/setup_2fa/setup_2fa.dart @@ -52,7 +52,8 @@ class Setup2FAPage extends BasePage { SizedBox(height: 86), SettingsCellWithArrow( title: S.current.setup_totp_recommended, - handler: (_) => Navigator.of(context).pushNamed(Routes.setup_2faQRPage), + handler: (_) => Navigator.of(context) + .pushReplacementNamed(Routes.setup_2faQRPage), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), ], diff --git a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart index e88b1090b..c4bee2b12 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart @@ -18,7 +18,8 @@ import 'package:mobx/mobx.dart'; import '../../../palette.dart'; import '../../../routes.dart'; -typedef OnTotpAuthenticationFinished = void Function(bool, TotpAuthCodePageState); +typedef OnTotpAuthenticationFinished = void Function( + bool, TotpAuthCodePageState); class TotpAuthCodePage extends StatefulWidget { TotpAuthCodePage( @@ -43,8 +44,9 @@ class TotpAuthCodePageState extends State { @override void initState() { - if(widget.totpArguments.onTotpAuthenticationFinished != null) { - _reaction ??= reaction((_) => widget.setup2FAViewModel.state, (ExecutionState state) { + if (widget.totpArguments.onTotpAuthenticationFinished != null) { + _reaction ??= reaction((_) => widget.setup2FAViewModel.state, + (ExecutionState state) { WidgetsBinding.instance.addPostFrameCallback((_) { if (state is ExecutedSuccessfullyState) { widget.totpArguments.onTotpAuthenticationFinished!(true, this); @@ -57,9 +59,9 @@ class TotpAuthCodePageState extends State { if (state is AuthenticationBanned) { widget.totpArguments.onTotpAuthenticationFinished!(false, this); - } - }); - }); + } + }); + }); } super.initState(); @@ -73,7 +75,8 @@ class TotpAuthCodePageState extends State { void changeProcessText(String text) { dismissFlushBar(_authBar); - _progressBar = createBar(text, duration: null)..show(_key.currentContext!); + _progressBar = createBar(text, duration: null) + ..show(_key.currentContext!); } Future close({String? route, dynamic arguments}) async { @@ -82,7 +85,8 @@ class TotpAuthCodePageState extends State { } await Future.delayed(Duration(milliseconds: 50)); if (route != null) { - Navigator.of(_key.currentContext!).pushReplacementNamed(route, arguments: arguments); + Navigator.of(_key.currentContext!) + .pushReplacementNamed(route, arguments: arguments); } else { Navigator.of(_key.currentContext!).pop(); } @@ -120,7 +124,8 @@ class TOTPEnterCode extends BasePage { } @override - String get title => isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa; + String get title => + isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa; Widget? leading(BuildContext context) { return isClosable ? super.leading(context) : null; @@ -166,21 +171,24 @@ class TOTPEnterCode extends BasePage { return PrimaryButton( isDisabled: setup2FAViewModel.enteredOTPCode.length != 8, onPressed: () async { - final result = - await setup2FAViewModel.totp2FAAuth(totpController.text, isForSetup); - final bannedState = setup2FAViewModel.state is AuthenticationBanned; + final result = await setup2FAViewModel.totp2FAAuth( + totpController.text, isForSetup); + final bannedState = + setup2FAViewModel.state is AuthenticationBanned; await showPopUp( context: context, builder: (BuildContext context) { return PopUpCancellableAlertDialog( - contentText: _textDisplayedInPopupOnResult(result, bannedState, context), + contentText: _textDisplayedInPopupOnResult( + result, bannedState, context), actionButtonText: S.of(context).ok, buttonAction: () { result ? setup2FAViewModel.success() : null; if (isForSetup && result) { - Navigator.pushNamedAndRemoveUntil( - context, Routes.dashboard, (route) => false); + Navigator.pop(context); + // Navigator.of(context) + // .popAndPushNamed(Routes.modify2FAPage); } else { Navigator.of(context).pop(result); } @@ -188,6 +196,11 @@ class TOTPEnterCode extends BasePage { ); }, ); + if (isForSetup && result) { + Navigator.pushReplacementNamed( + context, Routes.modify2FAPage); + } + }, text: S.of(context).continue_text, color: Theme.of(context).accentTextTheme.bodyLarge!.color!, @@ -201,10 +214,13 @@ class TOTPEnterCode extends BasePage { ); } - String _textDisplayedInPopupOnResult(bool result, bool bannedState, BuildContext context) { + String _textDisplayedInPopupOnResult( + bool result, bool bannedState, BuildContext context) { switch (result) { case true: - return isForSetup ? S.current.totp_2fa_success : S.current.totp_verification_success; + return isForSetup + ? S.current.totp_2fa_success + : S.current.totp_verification_success; case false: if (bannedState) { final state = setup2FAViewModel.state as AuthenticationBanned; diff --git a/lib/src/screens/wallet/wallet_edit_page.dart b/lib/src/screens/wallet/wallet_edit_page.dart index 3ff27f02f..64575b722 100644 --- a/lib/src/screens/wallet/wallet_edit_page.dart +++ b/lib/src/screens/wallet/wallet_edit_page.dart @@ -125,7 +125,9 @@ class WalletEditPage extends BasePage { } _onSuccessfulAuth(context); - }); + }, + conditionToDetermineIfToUse2FA: false, + ); } void _onSuccessfulAuth(BuildContext context) async { diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index c1f684898..8849529e2 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -55,7 +55,7 @@ class WalletListBodyState extends State { final newWalletImage = Image.asset('assets/images/new_wallet.png', height: 12, width: 12, color: Colors.white); final restoreWalletImage = Image.asset('assets/images/restore_wallet.png', - height: 12, width: 12, color: Theme.of(context).primaryTextTheme!.titleLarge!.color!); + height: 12, width: 12, color: Theme.of(context).primaryTextTheme.titleLarge!.color!); return Container( padding: EdgeInsets.only(top: 16), @@ -72,7 +72,7 @@ class WalletListBodyState extends State { itemBuilder: (__, index) { final wallet = widget.walletListViewModel.wallets[index]; final currentColor = wallet.isCurrent - ? Theme.of(context).accentTextTheme!.titleSmall!.decorationColor! + ? Theme.of(context).accentTextTheme.titleSmall!.decorationColor! : Theme.of(context).colorScheme.background; final row = GestureDetector( onTap: () => wallet.isCurrent ? null : _loadWallet(wallet), @@ -131,8 +131,7 @@ class WalletListBodyState extends State { : Row(children: [ Expanded(child: row), GestureDetector( - onTap: () => Navigator.of(context).pushNamed( - Routes.walletEdit, + onTap: () => Navigator.of(context).pushNamed(Routes.walletEdit, arguments: [widget.walletListViewModel, wallet]), child: Container( padding: EdgeInsets.only(right: 20), @@ -150,10 +149,7 @@ class WalletListBodyState extends State { child: Icon( Icons.edit, size: 14, - color: Theme.of(context) - .textTheme - .headlineMedium! - .color!, + color: Theme.of(context).textTheme.headlineMedium!.color!, ), ), ), @@ -167,27 +163,59 @@ class WalletListBodyState extends State { bottomSection: Column(children: [ PrimaryImageButton( onPressed: () { + //TODO(David): Find a way to optimize this if (isSingleCoin) { - Navigator.of(context).pushNamed(Routes.newWallet, - arguments: widget.walletListViewModel.currentWalletType); + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.newWallet, + arguments: widget.walletListViewModel.currentWalletType, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: widget.walletListViewModel.currentWalletType, + ); + } } else { - Navigator.of(context).pushNamed(Routes.newWalletType); + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.newWalletType, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed(Routes.newWalletType); + } } }, image: newWalletImage, text: S.of(context).wallet_list_create_new_wallet, - color: Theme.of(context).accentTextTheme!.bodyLarge!.color!, + color: Theme.of(context).accentTextTheme.bodyLarge!.color!, textColor: Colors.white, ), SizedBox(height: 10.0), PrimaryImageButton( onPressed: () { - Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.restoreOptions, + arguments: false, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); + } }, image: restoreWalletImage, text: S.of(context).wallet_list_restore_wallet, - color: Theme.of(context).accentTextTheme!.bodySmall!.color!, - textColor: Theme.of(context).primaryTextTheme!.titleLarge!.color!) + color: Theme.of(context).accentTextTheme.bodySmall!.color!, + textColor: Theme.of(context).primaryTextTheme.titleLarge!.color!) ])), ); } @@ -208,27 +236,31 @@ class WalletListBodyState extends State { } Future _loadWallet(WalletListItem wallet) async { - await widget.authService.authenticateAction(context, - onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) { - return; - } - - try { - changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); - await widget.walletListViewModel.loadWallet(wallet); - await hideProgressText(); - // only pop the wallets route in mobile as it will go back to dashboard page - // in desktop platforms the navigation tree is different - if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.of(context).pop(); - }); + await widget.authService.authenticateAction( + context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (!isAuthenticatedSuccessfully) { + return; } - } catch (e) { - changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); - } - }); + + try { + changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); + await widget.walletListViewModel.loadWallet(wallet); + await hideProgressText(); + // only pop the wallets route in mobile as it will go back to dashboard page + // in desktop platforms the navigation tree is different + if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(); + }); + } + } catch (e) { + changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); + } + }, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, + ); } void changeProcessText(String text) { diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 87e2cee4c..ec0a7fe5e 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.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/pin_code_required_duration.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; @@ -56,6 +57,15 @@ abstract class SettingsStoreBase with Store { required this.isBitcoinBuyEnabled, required this.actionlistDisplayMode, required this.pinTimeOutDuration, + required Cake2FAPresetsOptions initialCake2FAPresetOptions, + required bool initialShouldRequireTOTP2FAForAccessingWallet, + required bool initialShouldRequireTOTP2FAForSendsToContact, + required bool initialShouldRequireTOTP2FAForSendsToNonContact, + required bool initialShouldRequireTOTP2FAForSendsToInternalWallets, + required bool initialShouldRequireTOTP2FAForExchangesToInternalWallets, + required bool initialShouldRequireTOTP2FAForAddingContacts, + required bool initialShouldRequireTOTP2FAForCreatingNewWallets, + required bool initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialHavenTransactionPriority, @@ -67,6 +77,7 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress = initialSaveRecipientAddress, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, + selectedCake2FAPreset = initialCake2FAPresetOptions, totpSecretKey = initialTotpSecretKey, useTOTP2FA = initialUseTOTP2FA, numberOfFailedTokenTrials = initialFailedTokenTrial, @@ -78,6 +89,18 @@ abstract class SettingsStoreBase with Store { currentTheme = initialTheme, pinCodeLength = initialPinLength, languageCode = initialLanguageCode, + shouldRequireTOTP2FAForAccessingWallet = initialShouldRequireTOTP2FAForAccessingWallet, + shouldRequireTOTP2FAForSendsToContact = initialShouldRequireTOTP2FAForSendsToContact, + shouldRequireTOTP2FAForSendsToNonContact = initialShouldRequireTOTP2FAForSendsToNonContact, + shouldRequireTOTP2FAForSendsToInternalWallets = + initialShouldRequireTOTP2FAForSendsToInternalWallets, + shouldRequireTOTP2FAForExchangesToInternalWallets = + initialShouldRequireTOTP2FAForExchangesToInternalWallets, + shouldRequireTOTP2FAForAddingContacts = initialShouldRequireTOTP2FAForAddingContacts, + shouldRequireTOTP2FAForCreatingNewWallets = + initialShouldRequireTOTP2FAForCreatingNewWallets, + shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, priority = ObservableMap() { //this.nodes = ObservableMap.of(nodes); @@ -166,6 +189,57 @@ abstract class SettingsStoreBase with Store { (bool biometricalAuthentication) => sharedPreferences.setBool( PreferencesKey.allowBiometricalAuthenticationKey, biometricalAuthentication)); + reaction( + (_) => selectedCake2FAPreset, + (Cake2FAPresetsOptions selectedCake2FAPreset) => sharedPreferences.setInt( + PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset.serialize())); + + reaction( + (_) => shouldRequireTOTP2FAForAccessingWallet, + (bool requireTOTP2FAForAccessingWallet) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForAccessingWallet, + requireTOTP2FAForAccessingWallet)); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToContact, + (bool requireTOTP2FAForSendsToContact) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForSendsToContact, requireTOTP2FAForSendsToContact)); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToNonContact, + (bool requireTOTP2FAForSendsToNonContact) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact, + requireTOTP2FAForSendsToNonContact)); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToInternalWallets, + (bool requireTOTP2FAForSendsToInternalWallets) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets, + requireTOTP2FAForSendsToInternalWallets)); + + reaction( + (_) => shouldRequireTOTP2FAForExchangesToInternalWallets, + (bool requireTOTP2FAForExchangesToInternalWallets) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + requireTOTP2FAForExchangesToInternalWallets)); + + reaction( + (_) => shouldRequireTOTP2FAForAddingContacts, + (bool requireTOTP2FAForAddingContacts) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForAddingContacts, requireTOTP2FAForAddingContacts)); + + reaction( + (_) => shouldRequireTOTP2FAForCreatingNewWallets, + (bool requireTOTP2FAForCreatingNewWallets) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets, + requireTOTP2FAForCreatingNewWallets)); + + reaction( + (_) => shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + (bool requireTOTP2FAForAllSecurityAndBackupSettings) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + requireTOTP2FAForAllSecurityAndBackupSettings)); + reaction( (_) => useTOTP2FA, (bool use) => sharedPreferences.setBool(PreferencesKey.useTOTP2FA, use)); @@ -249,6 +323,33 @@ abstract class SettingsStoreBase with Store { @observable bool allowBiometricalAuthentication; + @observable + bool shouldRequireTOTP2FAForAccessingWallet; + + @observable + bool shouldRequireTOTP2FAForSendsToContact; + + @observable + bool shouldRequireTOTP2FAForSendsToNonContact; + + @observable + bool shouldRequireTOTP2FAForSendsToInternalWallets; + + @observable + bool shouldRequireTOTP2FAForExchangesToInternalWallets; + + @observable + Cake2FAPresetsOptions selectedCake2FAPreset; + + @observable + bool shouldRequireTOTP2FAForAddingContacts; + + @observable + bool shouldRequireTOTP2FAForCreatingNewWallets; + + @observable + bool shouldRequireTOTP2FAForAllSecurityAndBackupSettings; + @observable String totpSecretKey; @@ -356,6 +457,29 @@ abstract class SettingsStoreBase with Store { FiatApiMode.enabled.raw); final allowBiometricalAuthentication = sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false; + final selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? + Cake2FAPresetsOptions.normal.raw); + final shouldRequireTOTP2FAForAccessingWallet = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false; + final shouldRequireTOTP2FAForSendsToContact = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false; + final shouldRequireTOTP2FAForSendsToNonContact = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false; + final shouldRequireTOTP2FAForSendsToInternalWallets = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ?? + false; + final shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? + false; + final shouldRequireTOTP2FAForAddingContacts = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; + final shouldRequireTOTP2FAForCreatingNewWallets = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ?? + false; + final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ?? + false; final totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? ''; final useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? false; final tokenTrialNumber = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? 0; @@ -433,6 +557,7 @@ abstract class SettingsStoreBase with Store { initialDisableSell: disableSell, initialFiatMode: currentFiatApiMode, initialAllowBiometricalAuthentication: allowBiometricalAuthentication, + initialCake2FAPresetOptions: selectedCake2FAPreset, initialTotpSecretKey: totpSecretKey, initialUseTOTP2FA: useTOTP2FA, initialFailedTokenTrial: tokenTrialNumber, @@ -446,6 +571,17 @@ abstract class SettingsStoreBase with Store { initialBitcoinTransactionPriority: bitcoinTransactionPriority, initialHavenTransactionPriority: havenTransactionPriority, initialLitecoinTransactionPriority: litecoinTransactionPriority, + initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, + initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, + initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, + initialShouldRequireTOTP2FAForSendsToInternalWallets: + shouldRequireTOTP2FAForSendsToInternalWallets, + initialShouldRequireTOTP2FAForExchangesToInternalWallets: + shouldRequireTOTP2FAForExchangesToInternalWallets, + initialShouldRequireTOTP2FAForAddingContacts: shouldRequireTOTP2FAForAddingContacts, + initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets, + initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings: + shouldRequireTOTP2FAForAllSecurityAndBackupSettings, shouldShowYatPopup: shouldShowYatPopup); } @@ -480,6 +616,7 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress; totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? totpSecretKey; useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? useTOTP2FA; + numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? @@ -490,9 +627,35 @@ abstract class SettingsStoreBase with Store { allowBiometricalAuthentication = sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? allowBiometricalAuthentication; + selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? + Cake2FAPresetsOptions.normal.raw); + shouldRequireTOTP2FAForAccessingWallet = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false; + shouldRequireTOTP2FAForSendsToContact = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false; + shouldRequireTOTP2FAForSendsToNonContact = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false; + shouldRequireTOTP2FAForSendsToInternalWallets = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ?? + false; + shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? + false; + shouldRequireTOTP2FAForAddingContacts = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; + shouldRequireTOTP2FAForCreatingNewWallets = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ?? + false; + shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ?? + false; shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? shouldShowMarketPlaceInDashboard; + selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? + Cake2FAPresetsOptions.narrow.raw); exchangeStatus = ExchangeApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); 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 5b0187fc8..65b939047 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:cake_wallet/entities/wallet_contact.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; @@ -11,10 +12,12 @@ import 'package:cw_core/crypto_currency.dart'; part 'contact_list_view_model.g.dart'; -class ContactListViewModel = ContactListViewModelBase with _$ContactListViewModel; +class ContactListViewModel = ContactListViewModelBase + with _$ContactListViewModel; abstract class ContactListViewModelBase with Store { - ContactListViewModelBase(this.contactSource, this.walletInfoSource, this._currency) + ContactListViewModelBase(this.contactSource, this.walletInfoSource, + this._currency, this.settingsStore) : contacts = ObservableList(), walletContacts = [] { walletInfoSource.values.forEach((info) { @@ -42,16 +45,23 @@ abstract class ContactListViewModelBase with Store { final List walletContacts; final CryptoCurrency? _currency; StreamSubscription? _subscription; + final SettingsStore settingsStore; bool get isEditable => _currency == null; + @computed + bool get shouldRequireTOTP2FAForAddingContacts => + settingsStore.shouldRequireTOTP2FAForAddingContacts; + Future delete(ContactRecord contact) async => contact.original.delete(); @computed - List get contactsToShow => - contacts.where((element) => _currency == null || element.type == _currency).toList(); + List get contactsToShow => contacts + .where((element) => _currency == null || element.type == _currency) + .toList(); @computed - List get walletContactsToShow => - walletContacts.where((element) => _currency == null || element.type == _currency).toList(); + List get walletContactsToShow => walletContacts + .where((element) => _currency == null || element.type == _currency) + .toList(); } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 23ba17ade..76e75bb58 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -4,12 +4,14 @@ import 'dart:convert'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart'; import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/simpleswap/simpleswap_request.dart'; import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trocador/trocador_request.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -44,42 +46,55 @@ part 'exchange_view_model.g.dart'; class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { - ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore, - this.tradesStore, this._settingsStore, this.sharedPreferences) - : _cryptoNumberFormat = NumberFormat(), - isFixedRateMode = false, - isReceiveAmountEntered = false, - depositAmount = '', - receiveAmount = '', - receiveAddress = '', - depositAddress = '', - isDepositAddressEnabled = false, - isReceiveAddressEnabled = false, - isReceiveAmountEditable = false, - _useTorOnly = false, - receiveCurrencies = [], - depositCurrencies = [], - limits = Limits(min: 0, max: 0), - tradeState = ExchangeTradeStateInitial(), - limitsState = LimitsInitialState(), - receiveCurrency = wallet.currency, - depositCurrency = wallet.currency, - providerList = [], - selectedProviders = ObservableList() { + ExchangeViewModelBase( + this.wallet, + this.trades, + this._exchangeTemplateStore, + this.tradesStore, + this._settingsStore, + this.sharedPreferences, + this.contactListViewModel) + : _cryptoNumberFormat = NumberFormat(), + isFixedRateMode = false, + isReceiveAmountEntered = false, + depositAmount = '', + receiveAmount = '', + receiveAddress = '', + depositAddress = '', + isDepositAddressEnabled = false, + isReceiveAddressEnabled = false, + isReceiveAmountEditable = false, + _useTorOnly = false, + receiveCurrencies = [], + depositCurrencies = [], + limits = Limits(min: 0, max: 0), + tradeState = ExchangeTradeStateInitial(), + limitsState = LimitsInitialState(), + receiveCurrency = wallet.currency, + depositCurrency = wallet.currency, + providerList = [], + selectedProviders = ObservableList() { _useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly; _setProviders(); const excludeDepositCurrencies = [CryptoCurrency.btt, CryptoCurrency.nano]; - const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, - CryptoCurrency.bnb, CryptoCurrency.btt, CryptoCurrency.nano]; + const excludeReceiveCurrencies = [ + CryptoCurrency.xlm, + CryptoCurrency.xrp, + CryptoCurrency.bnb, + CryptoCurrency.btt, + CryptoCurrency.nano + ]; _initialPairBasedOnWallet(); - final Map exchangeProvidersSelection = json - .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; + final Map exchangeProvidersSelection = json.decode( + sharedPreferences + .getString(PreferencesKey.exchangeProvidersSelection) ?? + "{}") as Map; /// if the provider is not in the user settings (user's first time or newly added provider) /// then use its default value decided by us - selectedProviders = ObservableList.of(providersForCurrentPair().where( - (element) => exchangeProvidersSelection[element.title] == null + selectedProviders = ObservableList.of(providersForCurrentPair() + .where((element) => exchangeProvidersSelection[element.title] == null ? element.isEnabled : (exchangeProvidersSelection[element.title] as bool)) .toList()); @@ -87,7 +102,8 @@ abstract class ExchangeViewModelBase with Store { _setAvailableProviders(); _calculateBestRate(); - bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate()); + bestRateSync = + Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate()); isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); @@ -95,7 +111,8 @@ abstract class ExchangeViewModelBase with Store { receiveAmount = ''; receiveAddress = ''; depositAddress = depositCurrency == wallet.currency - ? wallet.walletAddresses.address : ''; + ? wallet.walletAddresses.address + : ''; provider = providersForCurrentPair().first; final initialProvider = provider; provider!.checkIsAvailable().then((bool isAvailable) { @@ -107,20 +124,20 @@ abstract class ExchangeViewModelBase with Store { } }); receiveCurrencies = CryptoCurrency.all - .where((cryptoCurrency) => !excludeReceiveCurrencies.contains(cryptoCurrency)) - .toList(); + .where((cryptoCurrency) => + !excludeReceiveCurrencies.contains(cryptoCurrency)) + .toList(); depositCurrencies = CryptoCurrency.all - .where((cryptoCurrency) => !excludeDepositCurrencies.contains(cryptoCurrency)) - .toList(); + .where((cryptoCurrency) => + !excludeDepositCurrencies.contains(cryptoCurrency)) + .toList(); _defineIsReceiveAmountEditable(); loadLimits(); - reaction( - (_) => isFixedRateMode, - (Object _) { - loadLimits(); - _bestRate = 0; - _calculateBestRate(); - }); + reaction((_) => isFixedRateMode, (Object _) { + loadLimits(); + _bestRate = 0; + _calculateBestRate(); + }); } bool _useTorOnly; final WalletBase wallet; @@ -148,7 +165,8 @@ abstract class ExchangeViewModelBase with Store { /// initialize with descending comparator /// since we want largest rate first final SplayTreeMap _sortedAvailableProviders = - SplayTreeMap((double a, double b) => b.compareTo(a)); + SplayTreeMap( + (double a, double b) => b.compareTo(a)); final List _tradeAvailableProviders = []; @@ -207,6 +225,37 @@ abstract class ExchangeViewModelBase with Store { ObservableList get templates => _exchangeTemplateStore.templates; + @computed + List get walletContactsToShow => + contactListViewModel.walletContacts + .where((element) => + receiveCurrency == null || element.type == receiveCurrency) + .toList(); + + @action + bool checkIfWalletIsAnInternalWallet(String address) { + final walletContactList = walletContactsToShow + .where((element) => element.address == address) + .toList(); + + return walletContactList.isNotEmpty; + } + + @computed + bool get shouldDisplayTOTP2FAForExchangesToInternalWallet => + _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets; + + //* Still open to further optimize these checks + //* It works but can be made better + @action + bool shouldDisplayTOTP() { + final isInternalWallet = checkIfWalletIsAnInternalWallet(receiveAddress); + if (isInternalWallet) { + return shouldDisplayTOTP2FAForExchangesToInternalWallet; + } + return false; + } + @computed TransactionPriority get transactionPriority { @@ -219,21 +268,23 @@ abstract class ExchangeViewModelBase with Store { return priority; } - bool get hasAllAmount => (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) && depositCurrency == wallet.currency; - bool get isMoneroWallet => wallet.type == WalletType.monero; + bool get isMoneroWallet => wallet.type == WalletType.monero; - bool get isLowFee { + bool get isLowFee { switch (wallet.type) { case WalletType.monero: case WalletType.haven: - return transactionPriority == monero!.getMoneroTransactionPrioritySlow(); + return transactionPriority == + monero!.getMoneroTransactionPrioritySlow(); case WalletType.bitcoin: - return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow(); + return transactionPriority == + bitcoin!.getBitcoinTransactionPrioritySlow(); case WalletType.litecoin: - return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow(); + return transactionPriority == + bitcoin!.getLitecoinTransactionPrioritySlow(); default: return false; } @@ -247,6 +298,8 @@ abstract class ExchangeViewModelBase with Store { final SettingsStore _settingsStore; + final ContactListViewModel contactListViewModel; + double _bestRate = 0.0; late Timer bestRateSync; @@ -337,23 +390,24 @@ abstract class ExchangeViewModelBase with Store { } Future _calculateBestRate() async { - final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1; + final amount = + double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1; final _providers = _tradeAvailableProviders - .where((element) => !isFixedRateMode || element.supportsFixedRate).toList(); + .where((element) => !isFixedRateMode || element.supportsFixedRate) + .toList(); - final result = await Future.wait( - _providers.map((element) => element.fetchRate( - from: depositCurrency, - to: receiveCurrency, - amount: amount, - isFixedRateMode: isFixedRateMode, - isReceiveAmount: isFixedRateMode)) - ); + final result = await Future.wait(_providers.map((element) => + element.fetchRate( + from: depositCurrency, + to: receiveCurrency, + amount: amount, + isFixedRateMode: isFixedRateMode, + isReceiveAmount: isFixedRateMode))); _sortedAvailableProviders.clear(); - for (int i=0;i highestMax) { + if (highestMax != null && + (tempLimits.max ?? double.maxFinite) > highestMax) { highestMax = tempLimits.max; } } catch (e) { @@ -445,7 +494,7 @@ abstract class ExchangeViewModelBase with Store { settleMethod: receiveCurrency, depositAmount: isFixedRateMode ? receiveAmount.replaceAll(',', '.') - : depositAmount.replaceAll(',', '.'), + : depositAmount.replaceAll(',', '.'), settleAddress: receiveAddress, refundAddress: depositAddress, ); @@ -525,6 +574,7 @@ abstract class ExchangeViewModelBase with Store { tradesStore.setTrade(trade); await trades.add(trade); tradeState = TradeIsCreatedSuccessfully(trade: trade); + /// return after the first successful trade return; } catch (e) { @@ -555,9 +605,11 @@ abstract class ExchangeViewModelBase with Store { depositAmount = ''; receiveAmount = ''; depositAddress = depositCurrency == wallet.currency - ? wallet.walletAddresses.address : ''; + ? wallet.walletAddresses.address + : ''; receiveAddress = receiveCurrency == wallet.currency - ? wallet.walletAddresses.address : ''; + ? wallet.walletAddresses.address + : ''; isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); isFixedRateMode = false; @@ -576,7 +628,8 @@ abstract class ExchangeViewModelBase with Store { } final amount = availableBalance - fee; - changeDepositAmount(amount: bitcoin!.formatterBitcoinAmountToString(amount: amount)); + changeDepositAmount( + amount: bitcoin!.formatterBitcoinAmountToString(amount: amount)); } } @@ -612,8 +665,7 @@ abstract class ExchangeViewModelBase with Store { {required CryptoCurrency from, required CryptoCurrency to}) { final providers = providerList .where((provider) => provider.pairList - .where((pair) => - pair.from == from && pair.to == to) + .where((pair) => pair.from == from && pair.to == to) .isNotEmpty) .toList(); @@ -690,11 +742,14 @@ abstract class ExchangeViewModelBase with Store { _bestRate = 0; _calculateBestRate(); - final Map exchangeProvidersSelection = json - .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; + final Map exchangeProvidersSelection = json.decode( + sharedPreferences + .getString(PreferencesKey.exchangeProvidersSelection) ?? + "{}") as Map; for (var provider in providerList) { - exchangeProvidersSelection[provider.title] = selectedProviders.contains(provider); + exchangeProvidersSelection[provider.title] = + selectedProviders.contains(provider); } sharedPreferences.setString( @@ -705,15 +760,15 @@ abstract class ExchangeViewModelBase with Store { bool get isAvailableInSelected { final providersForPair = providersForCurrentPair(); - return selectedProviders.any((element) => element.isAvailable && providersForPair.contains(element)); + return selectedProviders.any( + (element) => element.isAvailable && providersForPair.contains(element)); } void _setAvailableProviders() { _tradeAvailableProviders.clear(); - _tradeAvailableProviders.addAll( - selectedProviders - .where((provider) => providersForCurrentPair().contains(provider))); + _tradeAvailableProviders.addAll(selectedProviders + .where((provider) => providersForCurrentPair().contains(provider))); } @action @@ -721,22 +776,27 @@ abstract class ExchangeViewModelBase with Store { switch (wallet.type) { case WalletType.monero: case WalletType.haven: - _settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic(); + _settingsStore.priority[wallet.type] = + monero!.getMoneroTransactionPriorityAutomatic(); break; case WalletType.bitcoin: - _settingsStore.priority[wallet.type] = bitcoin!.getBitcoinTransactionPriorityMedium(); + _settingsStore.priority[wallet.type] = + bitcoin!.getBitcoinTransactionPriorityMedium(); break; case WalletType.litecoin: - _settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium(); + _settingsStore.priority[wallet.type] = + bitcoin!.getLitecoinTransactionPriorityMedium(); break; default: break; } } - void _setProviders(){ + void _setProviders() { if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) { - providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList(); + providerList = _allProviders + .where((provider) => provider.supportsOnionAddress) + .toList(); } else { providerList = _allProviders; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 248159b7d..85f94731e 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,6 +1,8 @@ -import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; +import 'package:cake_wallet/entities/wallet_contact.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/view_model/send/output.dart'; @@ -38,6 +40,7 @@ abstract class SendViewModelBase with Store { this.sendTemplateViewModel, this._fiatConversationStore, this.balanceViewModel, + this.contactListViewModel, this.transactionDescriptionBox) : state = InitialExecutionState(), currencies = _wallet.balance.keys.toList(), @@ -50,8 +53,9 @@ abstract class SendViewModelBase with Store { if (!priorityForWalletType(_wallet.type).contains(priority)) { _settingsStore.priority[_wallet.type] = priorities.first; } - - outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); + + outputs + .add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); } @observable @@ -61,7 +65,8 @@ abstract class SendViewModelBase with Store { @action void addOutput() { - outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); + outputs + .add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); } @action @@ -148,13 +153,11 @@ abstract class SendViewModelBase with Store { @computed String get pendingTransactionFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFiatAmount + - ' ' + fiat.title; + isFiatDisabled ? '' : pendingTransactionFiatAmount + ' ' + fiat.title; @computed String get pendingTransactionFeeFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + - ' ' + fiat.title; + isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title; @computed bool get isReadyForSend => _wallet.syncStatus is SyncedSyncStatus; @@ -175,9 +178,8 @@ abstract class SendViewModelBase with Store { bool get hasMultiRecipient => _wallet.type != WalletType.haven; - bool get hasYat => outputs.any((out) => - out.isParsedAddress && - out.parsedAddress.parseFrom == ParseFrom.yatRecord); + bool get hasYat => outputs + .any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord); WalletType get walletType => _wallet.type; @@ -193,9 +195,73 @@ abstract class SendViewModelBase with Store { final SettingsStore _settingsStore; final SendTemplateViewModel sendTemplateViewModel; final BalanceViewModel balanceViewModel; + final ContactListViewModel contactListViewModel; final FiatConversionStore _fiatConversationStore; final Box transactionDescriptionBox; + @computed + List get contactsToShow => contactListViewModel.contacts + .where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency) + .toList(); + + @computed + List get walletContactsToShow => contactListViewModel.walletContacts + .where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency) + .toList(); + + @action + bool checkIfAddressIsAContact(String address) { + final contactList = contactsToShow.where((element) => element.address == address).toList(); + + return contactList.isNotEmpty; + } + + @action + bool checkIfWalletIsAnInternalWallet(String address) { + final walletContactList = + walletContactsToShow.where((element) => element.address == address).toList(); + + return walletContactList.isNotEmpty; + } + + @computed + bool get shouldDisplayTOTP2FAForContact => _settingsStore.shouldRequireTOTP2FAForSendsToContact; + + @computed + bool get shouldDisplayTOTP2FAForNonContact => + _settingsStore.shouldRequireTOTP2FAForSendsToNonContact; + + @computed + bool get shouldDisplayTOTP2FAForSendsToInternalWallet => + _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets; + + //* Still open to further optimize these checks + //* It works but can be made better + @action + bool checkThroughChecksToDisplayTOTP(String address) { + final isContact = checkIfAddressIsAContact(address); + final isInternalWallet = checkIfWalletIsAnInternalWallet(address); + + if (isContact) { + return shouldDisplayTOTP2FAForContact; + } else if (isInternalWallet) { + return shouldDisplayTOTP2FAForSendsToInternalWallet; + } else { + return shouldDisplayTOTP2FAForNonContact; + } + } + + bool shouldDisplayTotp() { + List conditionsList = []; + + for (var output in outputs) { + final show = checkThroughChecksToDisplayTOTP(output.address); + conditionsList.add(show); + } + + return conditionsList.contains(true); + } + @action Future createTransaction() async { try { @@ -234,11 +300,9 @@ abstract class SendViewModelBase with Store { if (pendingTransaction!.id.isNotEmpty) { _settingsStore.shouldSaveRecipientAddress ? await transactionDescriptionBox.add(TransactionDescription( - id: pendingTransaction!.id, - recipientAddress: address, - transactionNote: note)) - : await transactionDescriptionBox.add(TransactionDescription( - id: pendingTransaction!.id, transactionNote: note)); + id: pendingTransaction!.id, recipientAddress: address, transactionNote: note)) + : await transactionDescriptionBox + .add(TransactionDescription(id: pendingTransaction!.id, transactionNote: note)); } state = TransactionCommitted(); @@ -276,15 +340,15 @@ abstract class SendViewModelBase with Store { throw Exception('Priority is null for wallet type: ${_wallet.type}'); } - return monero!.createMoneroTransactionCreationCredentials( - outputs: outputs, priority: priority); + return monero! + .createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority); case WalletType.haven: final priority = _settingsStore.priority[_wallet.type]; if (priority == null) { throw Exception('Priority is null for wallet type: ${_wallet.type}'); } - + return haven!.createHavenTransactionCreationCredentials( outputs: outputs, priority: priority, assetType: selectedCryptoCurrency.title); default: @@ -304,14 +368,12 @@ abstract class SendViewModelBase with Store { return priority.toString(); } - bool _isEqualCurrency(String currency) => + bool _isEqualCurrency(String currency) => currency.toLowerCase() == _wallet.currency.title.toLowerCase(); @action - void onClose() => - _settingsStore.fiatCurrency = fiatFromSettings; + void onClose() => _settingsStore.fiatCurrency = fiatFromSettings; @action - void setFiatCurrency(FiatCurrency fiat) => - _settingsStore.fiatCurrency = fiat; + void setFiatCurrency(FiatCurrency fiat) => _settingsStore.fiatCurrency = fiat; } diff --git a/lib/view_model/set_up_2fa_viewmodel.dart b/lib/view_model/set_up_2fa_viewmodel.dart index e3ca53660..96a0c4a20 100644 --- a/lib/view_model/set_up_2fa_viewmodel.dart +++ b/lib/view_model/set_up_2fa_viewmodel.dart @@ -1,5 +1,6 @@ // ignore_for_file: prefer_final_fields +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/totp_utils.dart' as Utils; import 'package:cake_wallet/view_model/auth_state.dart'; @@ -23,8 +24,11 @@ abstract class Setup2FAViewModelBase with Store { Setup2FAViewModelBase(this._settingsStore, this._sharedPreferences, this._authService) : _failureCounter = 0, enteredOTPCode = '', + unhighlightTabs = false, + selected2FASettings = ObservableList(), state = InitialExecutionState() { _getRandomBase32SecretKey(); + selectCakePreset(selectedCake2FAPreset); reaction((_) => state, _saveLastAuthTime); } @@ -48,6 +52,38 @@ abstract class Setup2FAViewModelBase with Store { @computed bool get useTOTP2FA => _settingsStore.useTOTP2FA; + @computed + bool get shouldRequireTOTP2FAForAccessingWallet => + _settingsStore.shouldRequireTOTP2FAForAccessingWallet; + + @computed + bool get shouldRequireTOTP2FAForSendsToContact => + _settingsStore.shouldRequireTOTP2FAForSendsToContact; + + @computed + bool get shouldRequireTOTP2FAForSendsToNonContact => + _settingsStore.shouldRequireTOTP2FAForSendsToNonContact; + + @computed + bool get shouldRequireTOTP2FAForSendsToInternalWallets => + _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets; + + @computed + bool get shouldRequireTOTP2FAForExchangesToInternalWallets => + _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets; + + @computed + bool get shouldRequireTOTP2FAForAddingContacts => + _settingsStore.shouldRequireTOTP2FAForAddingContacts; + + @computed + bool get shouldRequireTOTP2FAForCreatingNewWallets => + _settingsStore.shouldRequireTOTP2FAForCreatingNewWallets; + + @computed + bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings => + _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings; + void _getRandomBase32SecretKey() { final randomBase32Key = Utils.generateRandomBase32SecretKey(16); _setBase32SecretKey(randomBase32Key); @@ -156,4 +192,230 @@ abstract class Setup2FAViewModelBase with Store { _authService.saveLastAuthTime(); } } + + @computed + Cake2FAPresetsOptions get selectedCake2FAPreset => _settingsStore.selectedCake2FAPreset; + + @observable + bool unhighlightTabs = false; + + @observable + ObservableList selected2FASettings; + + //! The code here works, but can be improved + //! Still trying out various ways to improve it + @action + void selectCakePreset(Cake2FAPresetsOptions cake2FAPreset) { + // The tabs are ordered in the format [Narrow || Normal || Verbose] + // Where Narrow = 0, Normal = 1 and Verbose = 2 + switch (cake2FAPreset) { + case Cake2FAPresetsOptions.narrow: + activateCake2FANarrowPreset(); + break; + case Cake2FAPresetsOptions.normal: + activateCake2FANormalPreset(); + break; + case Cake2FAPresetsOptions.aggressive: + activateCake2FAAggressivePreset(); + break; + default: + activateCake2FANormalPreset(); + } + } + + @action + void checkIfTheCurrentSettingMatchesAnyOfThePresets() { + final hasNormalPreset = checkIfTheNormalPresetIsPresent(); + final hasNarrowPreset = checkIfTheNarrowPresetIsPresent(); + final hasVerbosePreset = checkIfTheVerbosePresetIsPresent(); + + if (hasNormalPreset || hasNarrowPreset || hasVerbosePreset) { + unhighlightTabs = false; + } else { + unhighlightTabs = true; + } + } + + @action + bool checkIfTheNormalPresetIsPresent() { + final hasContacts = selected2FASettings.contains(VerboseControlSettings.sendsToContacts); + final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts); + final hasSecurityAndBackup = + selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings); + + final hasSendToInternalWallet = + selected2FASettings.contains(VerboseControlSettings.sendsToInternalWallets); + + final hasExchangesToInternalWallet = + selected2FASettings.contains(VerboseControlSettings.exchangesToInternalWallets); + + bool isOnlyNormalPresetControlsPresent = selected2FASettings.length == 5; + + return (hasContacts && + hasNonContacts && + hasSecurityAndBackup && + hasSendToInternalWallet && + hasExchangesToInternalWallet && + isOnlyNormalPresetControlsPresent); + } + + @action + bool checkIfTheVerbosePresetIsPresent() { + final hasAccessWallets = selected2FASettings.contains(VerboseControlSettings.accessWallet); + final hasSecurityAndBackup = + selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings); + + bool isOnlyVerbosePresetControlsPresent = selected2FASettings.length == 2; + + return (hasAccessWallets && hasSecurityAndBackup && isOnlyVerbosePresetControlsPresent); + } + + @action + bool checkIfTheNarrowPresetIsPresent() { + final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts); + final hasAddContacts = selected2FASettings.contains(VerboseControlSettings.addingContacts); + final hasCreateNewWallet = + selected2FASettings.contains(VerboseControlSettings.creatingNewWallets); + final hasSecurityAndBackup = + selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings); + + bool isOnlyNarrowPresetControlsPresent = selected2FASettings.length == 4; + + return (hasNonContacts && + hasAddContacts && + hasCreateNewWallet && + hasSecurityAndBackup && + isOnlyNarrowPresetControlsPresent); + } + + @action + void activateCake2FANormalPreset() { + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.normal; + setAllControlsToFalse(); + switchShouldRequireTOTP2FAForSendsToNonContact(true); + switchShouldRequireTOTP2FAForSendsToContact(true); + switchShouldRequireTOTP2FAForSendsToInternalWallets(true); + switchShouldRequireTOTP2FAForExchangesToInternalWallets(true); + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true); + } + + @action + void activateCake2FANarrowPreset() { + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.narrow; + setAllControlsToFalse(); + switchShouldRequireTOTP2FAForSendsToNonContact(true); + switchShouldRequireTOTP2FAForAddingContacts(true); + switchShouldRequireTOTP2FAForCreatingNewWallet(true); + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true); + } + + @action + void activateCake2FAAggressivePreset() { + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.aggressive; + setAllControlsToFalse(); + switchShouldRequireTOTP2FAForAccessingWallet(true); + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true); + } + + @action + void setAllControlsToFalse() { + switchShouldRequireTOTP2FAForAccessingWallet(false); + switchShouldRequireTOTP2FAForSendsToContact(false); + switchShouldRequireTOTP2FAForSendsToNonContact(false); + switchShouldRequireTOTP2FAForAddingContacts(false); + switchShouldRequireTOTP2FAForCreatingNewWallet(false); + switchShouldRequireTOTP2FAForExchangesToInternalWallets(false); + switchShouldRequireTOTP2FAForSendsToInternalWallets(false); + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(false); + selected2FASettings.clear(); + unhighlightTabs = false; + } + + @action + void switchShouldRequireTOTP2FAForAccessingWallet(bool value) { + _settingsStore.shouldRequireTOTP2FAForAccessingWallet = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.accessWallet); + } else { + selected2FASettings.remove(VerboseControlSettings.accessWallet); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForSendsToContact(bool value) { + _settingsStore.shouldRequireTOTP2FAForSendsToContact = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.sendsToContacts); + } else { + selected2FASettings.remove(VerboseControlSettings.sendsToContacts); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForSendsToNonContact(bool value) { + _settingsStore.shouldRequireTOTP2FAForSendsToNonContact = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.sendsToNonContacts); + } else { + selected2FASettings.remove(VerboseControlSettings.sendsToNonContacts); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForSendsToInternalWallets(bool value) { + _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.sendsToInternalWallets); + } else { + selected2FASettings.remove(VerboseControlSettings.sendsToInternalWallets); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForExchangesToInternalWallets(bool value) { + _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.exchangesToInternalWallets); + } else { + selected2FASettings.remove(VerboseControlSettings.exchangesToInternalWallets); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForAddingContacts(bool value) { + _settingsStore.shouldRequireTOTP2FAForAddingContacts = value; + if (value) + selected2FASettings.add(VerboseControlSettings.addingContacts); + else { + selected2FASettings.remove(VerboseControlSettings.addingContacts); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForCreatingNewWallet(bool value) { + _settingsStore.shouldRequireTOTP2FAForCreatingNewWallets = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.creatingNewWallets); + } else { + selected2FASettings.remove(VerboseControlSettings.creatingNewWallets); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(bool value) { + _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings = value; + if (value) + selected2FASettings.add(VerboseControlSettings.securityAndBackupSettings); + else { + selected2FASettings.remove(VerboseControlSettings.securityAndBackupSettings); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } } diff --git a/lib/view_model/settings/security_settings_view_model.dart b/lib/view_model/settings/security_settings_view_model.dart index f1a6551e3..5ea4dd4ea 100644 --- a/lib/view_model/settings/security_settings_view_model.dart +++ b/lib/view_model/settings/security_settings_view_model.dart @@ -24,6 +24,10 @@ abstract class SecuritySettingsViewModelBase with Store { @computed bool get useTotp2FA => _settingsStore.useTOTP2FA; + @computed + bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings => + _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings; + @computed PinCodeRequiredDuration get pinCodeRequiredDuration => _settingsStore.pinTimeOutDuration; 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 b12fe0c90..1f6a0540a 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -27,6 +27,14 @@ abstract class WalletListViewModelBase with Store { @observable ObservableList wallets; + @computed + bool get shouldRequireTOTP2FAForAccessingWallet => + _appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; + + @computed + bool get shouldRequireTOTP2FAForCreatingNewWallets => + _appStore.settingsStore.shouldRequireTOTP2FAForCreatingNewWallets; + final AppStore _appStore; final Box _walletInfoSource; final WalletLoadingService _walletLoadingService; @@ -38,7 +46,6 @@ abstract class WalletListViewModelBase with Store { Future loadWallet(WalletListItem walletItem) async { final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name); - _appStore.changeCurrentWallet(wallet); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c55241797..327f18db5 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,6 +12,7 @@ import devicelocale import flutter_secure_storage_macos import in_app_review import package_info +import package_info_plus import path_provider_foundation import platform_device_id import platform_device_id_macos @@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) + FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin")) PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin")) diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index d81a3dade..8f316f9a7 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -630,6 +630,18 @@ "setup_totp_recommended": "إعداد TOTP (موصى به)", "disable_buy": "تعطيل إجراء الشراء", "disable_sell": "قم بتعطيل إجراء البيع", + "cake_2fa_preset" : " كعكة 2FA مسبقا", + "narrow": "ضيق", + "normal": "طبيعي", + "aggressive": "عنيف", + "require_for_assessing_wallet": "تتطلب الوصول إلى المحفظة", + "require_for_sends_to_non_contacts" : "تتطلب لارسال لغير جهات الاتصال", + "require_for_sends_to_contacts" : "تتطلب لارسال جهات الاتصال", + "require_for_sends_to_internal_wallets" : "تتطلب عمليات الإرسال إلى المحافظ الداخلية", + "require_for_exchanges_to_internal_wallets" : "تتطلب عمليات التبادل إلى المحافظ الداخلية", + "require_for_adding_contacts" : "تتطلب إضافة جهات اتصال", + "require_for_creating_new_wallets" : "تتطلب إنشاء محافظ جديدة", + "require_for_all_security_and_backup_settings" : "مطلوب لجميع إعدادات الأمان والنسخ الاحتياطي", "available_balance_description": "الرصيد المتاح هو الرصيد الذي يمكنك إنفاقه أو تحويله إلى محفظة أخرى. يتم تجميد الرصيد المتاح للمعاملات الصادرة والمعاملات الواردة غير المؤكدة.", "syncing_wallet_alert_title": "محفظتك تتم مزامنتها", "syncing_wallet_alert_content": "قد لا يكتمل رصيدك وقائمة المعاملات الخاصة بك حتى تظهر عبارة “SYNCHRONIZED“ في الأعلى. انقر / اضغط لمعرفة المزيد.", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 778e9d7a6..be67c205b 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -626,6 +626,18 @@ "setup_totp_recommended": "Настройка на TOTP (препоръчително)", "disable_buy": "Деактивирайте действието за покупка", "disable_sell": "Деактивирайте действието за продажба", + "cake_2fa_preset" : "Торта 2FA Preset", + "narrow": "Тесен", + "normal": "нормално", + "aggressive": "Прекалено усърден", + "require_for_assessing_wallet": "Изискване за достъп до портфейла", + "require_for_sends_to_non_contacts" : "Изискване за изпращане до лица без контакт", + "require_for_sends_to_contacts" : "Изискване за изпращане до контакти", + "require_for_sends_to_internal_wallets" : "Изискване за изпращане до вътрешни портфейли", + "require_for_exchanges_to_internal_wallets" : "Изискване за обмен към вътрешни портфейли", + "require_for_adding_contacts" : "Изисква се за добавяне на контакти", + "require_for_creating_new_wallets" : "Изискване за създаване на нови портфейли", + "require_for_all_security_and_backup_settings" : "Изисква се за всички настройки за сигурност и архивиране", "available_balance_description": "Това е балансът, който можете да използвате за покупка на криптовалути. Това не включва замразените средства.", "syncing_wallet_alert_title": "Вашият портфейл се синхронизира", "syncing_wallet_alert_content": "Списъкът ви с баланс и транзакции може да не е пълен, докато в горната част не пише „СИНХРОНИЗИРАН“. Кликнете/докоснете, за да научите повече.", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 9186b5f45..454205ecd 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -626,6 +626,18 @@ "setup_totp_recommended": "Nastavit TOTP (doporučeno)", "disable_buy": "Zakázat akci nákupu", "disable_sell": "Zakázat akci prodeje", + "cake_2fa_preset" : "Předvolba Cake 2FA", + "narrow": "Úzký", + "normal": "Normální", + "aggressive": "Agresivní", + "require_for_assessing_wallet": "Vyžadovat pro přístup k peněžence", + "require_for_sends_to_non_contacts" : "Vyžadovat pro odesílání nekontaktním osobám", + "require_for_sends_to_contacts" : "Vyžadovat pro odeslání kontaktům", + "require_for_sends_to_internal_wallets" : "Vyžadovat pro odesílání do interních peněženek", + "require_for_exchanges_to_internal_wallets" : "Vyžadovat pro výměny do interních peněženek", + "require_for_adding_contacts" : "Vyžadovat pro přidání kontaktů", + "require_for_creating_new_wallets" : "Vyžadovat pro vytváření nových peněženek", + "require_for_all_security_and_backup_settings" : "Vyžadovat všechna nastavení zabezpečení a zálohování", "available_balance_description": "Dostupná částka je částka, kterou můžete okamžitě utratit. Zmrazená částka je částka, která ještě není k dispozici, protože ještě nebyla potvrzena síťovým protokolem.", "syncing_wallet_alert_title": "Vaše peněženka se synchronizuje", "syncing_wallet_alert_content": "Váš seznam zůstatků a transakcí nemusí být úplný, dokud nebude nahoře uvedeno „SYNCHRONIZOVANÉ“. Kliknutím/klepnutím se dozvíte více.", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index b3d7c9d21..5b74f3abd 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "TOTP einrichten (empfohlen)", "disable_buy": "Kaufaktion deaktivieren", "disable_sell": "Verkaufsaktion deaktivieren", + "cake_2fa_preset" : "Kuchen 2FA-Voreinstellung", + "narrow": "Eng", + "normal": "Normal", + "aggressive": "Übereifrig", + "require_for_assessing_wallet": "Für den Zugriff auf die Wallet erforderlich", + "require_for_sends_to_non_contacts" : "Erforderlich für Versendungen an Nichtkontakte", + "require_for_sends_to_contacts" : "Erforderlich für Versendungen an Kontakte", + "require_for_sends_to_internal_wallets" : "Erforderlich für Sendungen an interne Wallets", + "require_for_exchanges_to_internal_wallets" : "Erforderlich für den Umtausch in interne Wallets", + "require_for_adding_contacts" : "Erforderlich zum Hinzufügen von Kontakten", + "require_for_creating_new_wallets" : "Erforderlich zum Erstellen neuer Wallets", + "require_for_all_security_and_backup_settings" : "Für alle Sicherheits- und Sicherungseinstellungen erforderlich", "available_balance_description": "Verfügbarer Saldo ist der Betrag, den Sie sofort ausgeben können. Dieser Betrag kann sich ändern, wenn Sie eine Transaktion senden oder empfangen.", "syncing_wallet_alert_title": "Ihr Wallet wird synchronisiert", "syncing_wallet_alert_content": "Ihr Kontostand und Ihre Transaktionsliste sind möglicherweise erst vollständig, wenn oben „SYNCHRONISIERT“ steht. Klicken/tippen Sie, um mehr zu erfahren.", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1e695e69a..361f8872d 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "Set up TOTP (Recommended)", "disable_buy": "Disable buy action", "disable_sell": "Disable sell action", + "cake_2fa_preset" : "Cake 2FA Preset", + "narrow": "Narrow", + "normal": "Normal", + "aggressive": "Aggressive", + "require_for_assessing_wallet": "Require for accessing wallet", + "require_for_sends_to_non_contacts" : "Require for sends to non-contacts", + "require_for_sends_to_contacts" : "Require for sends to contacts", + "require_for_sends_to_internal_wallets" : "Require for sends to internal wallets", + "require_for_exchanges_to_internal_wallets" : "Require for exchanges to internal wallets", + "require_for_adding_contacts" : "Require for adding contacts", + "require_for_creating_new_wallets" : "Require for creating new wallets", + "require_for_all_security_and_backup_settings" : "Require for all security and backup settings", "available_balance_description": "The “Available Balance” or “Confirmed Balance” are funds that can be spent immediately. If funds appear in the lower balance but not the top balance, then you must wait a few minutes for the incoming funds to get more network confirmations. After they get more confirmations, they will be spendable.", "syncing_wallet_alert_title": "Your wallet is syncing", "syncing_wallet_alert_content": "Your balance and transaction list may not be complete until it says “SYNCHRONIZED” at the top. Click/tap to learn more.", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c8d4ba529..379f76349 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "Configurar TOTP (Recomendado)", "disable_buy": "Desactivar acción de compra", "disable_sell": "Desactivar acción de venta", + "cake_2fa_preset" : "Pastel 2FA preestablecido", + "narrow": "Angosto", + "normal": "Normal", + "aggressive": "Demasiado entusiasta", + "require_for_assessing_wallet": "Requerido para acceder a la billetera", + "require_for_sends_to_non_contacts" : "Requerido para envíos a no contactos", + "require_for_sends_to_contacts" : "Requerir para envíos a contactos", + "require_for_sends_to_internal_wallets" : "Requerido para envíos a billeteras internas", + "require_for_exchanges_to_internal_wallets" : "Requerido para intercambios a billeteras internas", + "require_for_adding_contacts" : "Requerido para agregar contactos", + "require_for_creating_new_wallets" : "Requerido para crear nuevas billeteras", + "require_for_all_security_and_backup_settings" : "Requerido para todas las configuraciones de seguridad y copia de seguridad", "available_balance_description": "Su saldo disponible es la cantidad de fondos que puede gastar. Los fondos que se muestran aquí se pueden gastar inmediatamente.", "syncing_wallet_alert_title": "Tu billetera se está sincronizando", "syncing_wallet_alert_content": "Es posible que su lista de saldo y transacciones no esté completa hasta que diga \"SINCRONIZADO\" en la parte superior. Haga clic/toque para obtener más información.", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index cdea0da7e..72868aecd 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "Configurer TOTP (recommandé)", "disable_buy": "Désactiver l'action d'achat", "disable_sell": "Désactiver l'action de vente", + "cake_2fa_preset" : "Gâteau 2FA prédéfini", + "narrow": "Étroit", + "normal": "Normal", + "aggressive": "Trop zélé", + "require_for_assessing_wallet": "Nécessaire pour accéder au portefeuille", + "require_for_sends_to_non_contacts" : "Exiger pour les envois à des non-contacts", + "require_for_sends_to_contacts" : "Exiger pour les envois aux contacts", + "require_for_sends_to_internal_wallets" : "Exiger pour les envois vers des portefeuilles internes", + "require_for_exchanges_to_internal_wallets" : "Exiger pour les échanges vers des portefeuilles internes", + "require_for_adding_contacts" : "Requis pour ajouter des contacts", + "require_for_creating_new_wallets" : "Nécessaire pour créer de nouveaux portefeuilles", + "require_for_all_security_and_backup_settings" : "Exiger pour tous les paramètres de sécurité et de sauvegarde", "available_balance_description": "Le solde disponible est le montant que vous pouvez dépenser immédiatement. Il est calculé en soustrayant le solde gelé du solde total.", "syncing_wallet_alert_title": "Votre portefeuille est en cours de synchronisation", "syncing_wallet_alert_content": "Votre solde et votre liste de transactions peuvent ne pas être complets tant qu'il n'y a pas « SYNCHRONISÉ » en haut. Cliquez/appuyez pour en savoir plus.", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index ce8b6f8f8..405d94082 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -612,6 +612,18 @@ "prevent_screenshots": "Fada lambobi da jarrabobi na kayan lambobi", "disable_buy": "Kashe alama", "disable_sell": "Kashe karbuwa", + "cake_2fa_preset" : "Cake 2FA saiti", + "narrow": "kunkuntar", + "normal": "Na al'ada", + "aggressive": "Mai tsananin kishi", + "require_for_assessing_wallet": "Bukatar samun damar walat", + "require_for_sends_to_non_contacts" : "Bukatar aika zuwa waɗanda ba lambobin sadarwa ba", + "require_for_sends_to_contacts" : "Bukatar aika zuwa lambobin sadarwa", + "require_for_sends_to_internal_wallets" : "Bukatar aika zuwa wallet na ciki", + "require_for_exchanges_to_internal_wallets" : "Bukatar musanya zuwa wallet na ciki", + "require_for_adding_contacts" : "Bukatar ƙara lambobin sadarwa", + "require_for_creating_new_wallets" : "Bukatar ƙirƙirar sabbin wallet", + "require_for_all_security_and_backup_settings" : "Bukatar duk tsaro da saitunan wariyar ajiya", "available_balance_description": "Ma'auni mai samuwa” ko ”,Tabbataccen Ma'auni”, kudade ne da za a iya kashewa nan da nan. Idan kudade sun bayyana a cikin ƙananan ma'auni amma ba babban ma'auni ba, to dole ne ku jira 'yan mintoci kaɗan don kudaden shiga don samun ƙarin tabbaci na hanyar sadarwa. Bayan sun sami ƙarin tabbaci, za a kashe su.", "syncing_wallet_alert_title": "Walat ɗin ku yana aiki tare", "syncing_wallet_alert_content": "Ma'aunin ku da lissafin ma'amala bazai cika ba har sai an ce \"SYNCHRONIZED\" a saman. Danna/matsa don ƙarin koyo.", @@ -621,4 +633,3 @@ "slidable": "Mai iya zamewa", "template_name": "Sunan Samfura" } - diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 652612b50..ace4a6141 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "टीओटीपी सेट अप करें (अनुशंसित)", "disable_buy": "खरीद कार्रवाई अक्षम करें", "disable_sell": "बेचने की कार्रवाई अक्षम करें", + "cake_2fa_preset" : "केक 2एफए प्रीसेट", + "narrow": "सँकरा", + "normal": "सामान्य", + "aggressive": "ज्यादा", + "require_for_assessing_wallet": "वॉलेट तक पहुँचने के लिए आवश्यकता है", + "require_for_sends_to_non_contacts" : "गैर-संपर्कों को भेजने की आवश्यकता", + "require_for_sends_to_contacts" : "संपर्कों को भेजने के लिए आवश्यक है", + "require_for_sends_to_internal_wallets" : "आंतरिक वॉलेट में भेजने की आवश्यकता है", + "require_for_exchanges_to_internal_wallets" : "आंतरिक वॉलेट में आदान-प्रदान की आवश्यकता है", + "require_for_adding_contacts" : "संपर्क जोड़ने के लिए आवश्यकता है", + "require_for_creating_new_wallets" : "नए वॉलेट बनाने की आवश्यकता है", + "require_for_all_security_and_backup_settings" : "सभी सुरक्षा और बैकअप सेटिंग्स की आवश्यकता है", "available_balance_description": "उपलब्ध शेष या ”पुष्टिकृत शेष”, वे धनराशि हैं जिन्हें तुरंत खर्च किया जा सकता है। यदि फंड निचले बैलेंस में दिखाई देते हैं, लेकिन शीर्ष बैलेंस में नहीं, तो आपको आने वाले फंड के लिए अधिक नेटवर्क पुष्टिकरण प्राप्त करने के लिए कुछ मिनट इंतजार करना होगा। अधिक पुष्टि मिलने के बाद, वे खर्च करने योग्य हो जाएंगे।", "syncing_wallet_alert_title": "आपका वॉलेट सिंक हो रहा है", "syncing_wallet_alert_content": "आपकी शेष राशि और लेनदेन सूची तब तक पूरी नहीं हो सकती जब तक कि शीर्ष पर \"सिंक्रनाइज़्ड\" न लिखा हो। अधिक जानने के लिए क्लिक/टैप करें।", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 6e6f094fe..2b6dd3aa6 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "Postavite TOTP (preporučeno)", "disable_buy": "Onemogući kupnju", "disable_sell": "Onemogući akciju prodaje", + "cake_2fa_preset" : "Cake 2FA Preset", + "narrow": "Usko", + "normal": "Normalno", + "aggressive": "Preterano", + "require_for_assessing_wallet": "Potreban za pristup novčaniku", + "require_for_sends_to_non_contacts" : "Zahtijeva za slanje nekontaktima", + "require_for_sends_to_contacts" : "Zahtijeva za slanje kontaktima", + "require_for_sends_to_internal_wallets" : "Zahtijeva za slanje u interne novčanike", + "require_for_exchanges_to_internal_wallets" : "Potreban za razmjenu na interne novčanike", + "require_for_adding_contacts" : "Zahtijeva za dodavanje kontakata", + "require_for_creating_new_wallets" : "Potreban za kreiranje novih novčanika", + "require_for_all_security_and_backup_settings" : "Zahtijeva za sve postavke sigurnosti i sigurnosne kopije", "available_balance_description": "Dostupno stanje je iznos koji možete potrošiti. To je vaš saldo minus bilo kakve transakcije koje su još uvijek u tijeku.", "syncing_wallet_alert_title": "Vaš novčanik se sinkronizira", "syncing_wallet_alert_content": "Vaš saldo i popis transakcija možda neće biti potpuni sve dok na vrhu ne piše \"SINKRONIZIRANO\". Kliknite/dodirnite da biste saznali više.", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index e55154882..7aaf7a695 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -622,6 +622,18 @@ "setup_totp_recommended": "Siapkan TOTP (Disarankan)", "disable_buy": "Nonaktifkan tindakan beli", "disable_sell": "Nonaktifkan aksi jual", + "cake_2fa_preset" : "Preset Kue 2FA", + "narrow": "Sempit", + "normal": "Normal", + "aggressive": "Terlalu bersemangat", + "require_for_assessing_wallet": "Diperlukan untuk mengakses dompet", + "require_for_sends_to_non_contacts" : "Wajibkan untuk mengirim ke non-kontak", + "require_for_sends_to_contacts" : "Membutuhkan untuk mengirim ke kontak", + "require_for_sends_to_internal_wallets" : "Diperlukan untuk mengirim ke dompet internal", + "require_for_exchanges_to_internal_wallets" : "Diperlukan untuk pertukaran ke dompet internal", + "require_for_adding_contacts" : "Membutuhkan untuk menambahkan kontak", + "require_for_creating_new_wallets" : "Diperlukan untuk membuat dompet baru", + "require_for_all_security_and_backup_settings" : "Memerlukan untuk semua pengaturan keamanan dan pencadangan", "available_balance_description": "“Saldo yang Tersedia” atau “Saldo yang Dikonfirmasi” adalah dana yang dapat langsung dibelanjakan. Jika dana muncul di saldo bawah tetapi tidak di saldo atas, maka Anda harus menunggu beberapa menit agar dana masuk mendapatkan konfirmasi jaringan lainnya. Setelah mereka mendapatkan lebih banyak konfirmasi, mereka akan dapat dibelanjakan.", "syncing_wallet_alert_title": "Dompet Anda sedang disinkronkan", "syncing_wallet_alert_content": "Saldo dan daftar transaksi Anda mungkin belum lengkap sampai tertulis “SYNCHRONIZED” di bagian atas. Klik/ketuk untuk mempelajari lebih lanjut.", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 1bd4e457f..f37f9069f 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "Imposta TOTP (consigliato)", "disable_buy": "Disabilita l'azione di acquisto", "disable_sell": "Disabilita l'azione di vendita", + "cake_2fa_preset" : "Torta 2FA Preset", + "narrow": "Stretto", + "normal": "Normale", + "aggressive": "Fervente", + "require_for_assessing_wallet": "Richiesto per l'accesso al portafoglio", + "require_for_sends_to_non_contacts" : "Richiesto per invii a non contatti", + "require_for_sends_to_contacts" : "Richiedi per gli invii ai contatti", + "require_for_sends_to_internal_wallets" : "Richiesto per invii a portafogli interni", + "require_for_exchanges_to_internal_wallets" : "Richiedi per gli scambi ai portafogli interni", + "require_for_adding_contacts" : "Richiesto per l'aggiunta di contatti", + "require_for_creating_new_wallets" : "Richiesto per la creazione di nuovi portafogli", + "require_for_all_security_and_backup_settings" : "Richiedi per tutte le impostazioni di sicurezza e backup", "available_balance_description": "Il saldo disponibile è il saldo totale meno i fondi congelati. I fondi congelati sono fondi che sono stati inviati ma non sono ancora stati confermati.", "syncing_wallet_alert_title": "Il tuo portafoglio si sta sincronizzando", "syncing_wallet_alert_content": "Il saldo e l'elenco delle transazioni potrebbero non essere completi fino a quando non viene visualizzato \"SYNCHRONIZED\" in alto. Clicca/tocca per saperne di più.", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index f90f208a6..3c5ba0ebf 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "TOTP を設定する (推奨)", "disable_buy": "購入アクションを無効にする", "disable_sell": "販売アクションを無効にする", + "cake_2fa_preset" : "ケーキ 2FA プリセット", + "narrow": "狭い", + "normal": "普通", + "aggressive": "熱心すぎる", + "require_for_assessing_wallet": "ウォレットにアクセスするために必要です", + "require_for_sends_to_non_contacts" : "非連絡先への送信に必須", + "require_for_sends_to_contacts" : "連絡先に送信する場合に必須", + "require_for_sends_to_internal_wallets" : "内部ウォレットへの送信に必須", + "require_for_exchanges_to_internal_wallets" : "内部ウォレットへの交換に必要", + "require_for_adding_contacts" : "連絡先の追加に必要", + "require_for_creating_new_wallets" : "新しいウォレットを作成するために必要です", + "require_for_all_security_and_backup_settings" : "すべてのセキュリティおよびバックアップ設定に必須", "available_balance_description": "利用可能な残高は、ウォレットの残高から冷凍残高を差し引いたものです。", "syncing_wallet_alert_title": "ウォレットは同期中です", "syncing_wallet_alert_content": "上部に「同期済み」と表示されるまで、残高と取引リストが完了していない可能性があります。詳細については、クリック/タップしてください。", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 5d4a497d5..f82daf22c 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "TOTP 설정(권장)", "disable_buy": "구매 행동 비활성화", "disable_sell": "판매 조치 비활성화", + "cake_2fa_preset" : "케이크 2FA 프리셋", + "narrow": "좁은", + "normal": "정상", + "aggressive": "지나치게 열심인", + "require_for_assessing_wallet": "지갑 접근을 위해 필요", + "require_for_sends_to_non_contacts" : "비접촉자에게 보내는 데 필요", + "require_for_sends_to_contacts" : "연락처로 보내기에 필요", + "require_for_sends_to_internal_wallets" : "내부 지갑으로 보내는 데 필요", + "require_for_exchanges_to_internal_wallets" : "내부 지갑으로의 교환에 필요", + "require_for_adding_contacts" : "연락처 추가에 필요", + "require_for_creating_new_wallets" : "새 지갑 생성에 필요", + "require_for_all_security_and_backup_settings" : "모든 보안 및 백업 설정에 필요", "available_balance_description": "이 지갑에서 사용할 수 있는 잔액입니다. 이 잔액은 블록체인에서 가져온 것이며, Cake Wallet이 사용할 수 없습니다.", "syncing_wallet_alert_title": "지갑 동기화 중", "syncing_wallet_alert_content": "상단에 \"동기화됨\"이라고 표시될 때까지 잔액 및 거래 목록이 완전하지 않을 수 있습니다. 자세히 알아보려면 클릭/탭하세요.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 2ee527cee..b6d148923 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "TOTP ကို ​​စနစ်ထည့်သွင်းပါ (အကြံပြုထားသည်)", "disable_buy": "ဝယ်ယူမှု လုပ်ဆောင်ချက်ကို ပိတ်ပါ။", "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။", + "cake_2fa_preset" : "ကိတ်မုန့် 2FA ကြိုတင်သတ်မှတ်", + "narrow": "ကျဉ်းသော", + "normal": "ပုံမှန်", + "aggressive": "စိတ်အားထက်သန်ခြင်း။", + "require_for_assessing_wallet": "ပိုက်ဆံအိတ်ကို ဝင်သုံးရန် လိုအပ်သည်။", + "require_for_sends_to_non_contacts" : "အဆက်အသွယ်မရှိသူများထံ ပေးပို့ရန် လိုအပ်သည်။", + "require_for_sends_to_contacts" : "အဆက်အသွယ်များထံ ပေးပို့ရန် လိုအပ်သည်။", + "require_for_sends_to_internal_wallets" : "အတွင်းပိုင်း ပိုက်ဆံအိတ်များသို့ ပေးပို့ရန် လိုအပ်သည်။", + "require_for_exchanges_to_internal_wallets" : "အတွင်းပိုင်းပိုက်ဆံအိတ်များသို့ လဲလှယ်ရန် လိုအပ်သည်။", + "require_for_adding_contacts" : "အဆက်အသွယ်များထည့်ရန် လိုအပ်သည်။", + "require_for_creating_new_wallets" : "ပိုက်ဆံအိတ်အသစ်များ ဖန်တီးရန် လိုအပ်သည်။", + "require_for_all_security_and_backup_settings" : "လုံခြုံရေးနှင့် အရန်ဆက်တင်များအားလုံးအတွက် လိုအပ်ပါသည်။", "available_balance_description": "သင့်ရဲ့ အကောင့်တွင် ရရှိနိုင်သော ငွေကျန်ငွေကို ပြန်လည်ပေးသွင်းပါ။", "syncing_wallet_alert_title": "သင့်ပိုက်ဆံအိတ်ကို စင့်ခ်လုပ်နေပါသည်။", "syncing_wallet_alert_content": "သင်၏လက်ကျန်နှင့် ငွေပေးငွေယူစာရင်းသည် ထိပ်တွင် \"Synchronizeed\" ဟုပြောသည်အထိ မပြီးမြောက်နိုင်ပါ။ ပိုမိုလေ့လာရန် နှိပ်/နှိပ်ပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 8b14ae06d..722a5a512 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "TOTP instellen (aanbevolen)", "disable_buy": "Koopactie uitschakelen", "disable_sell": "Verkoopactie uitschakelen", + "cake_2fa_preset" : "Taart 2FA Voorinstelling", + "narrow": "Smal", + "normal": "Normaal", + "aggressive": "Overijverig", + "require_for_assessing_wallet": "Vereist voor toegang tot portemonnee", + "require_for_sends_to_non_contacts" : "Vereist voor verzendingen naar niet-contacten", + "require_for_sends_to_contacts" : "Vereist voor verzending naar contacten", + "require_for_sends_to_internal_wallets" : "Vereist voor verzendingen naar interne portefeuilles", + "require_for_exchanges_to_internal_wallets" : "Vereist voor uitwisselingen naar interne portefeuilles", + "require_for_adding_contacts" : "Vereist voor het toevoegen van contacten", + "require_for_creating_new_wallets" : "Vereist voor het maken van nieuwe portefeuilles", + "require_for_all_security_and_backup_settings" : "Vereist voor alle beveiligings- en back-upinstellingen", "available_balance_description": "Beschikbaar saldo is het saldo dat u kunt uitgeven. Het kan lager zijn dan uw totale saldo als u onlangs geld hebt verzonden.", "syncing_wallet_alert_title": "Uw portemonnee wordt gesynchroniseerd", "syncing_wallet_alert_content": "Uw saldo- en transactielijst is mogelijk pas compleet als er bovenaan 'GESYNCHRONISEERD' staat. Klik/tik voor meer informatie.", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index c93305648..243e73cec 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "Skonfiguruj TOTP (zalecane)", "disable_buy": "Wyłącz akcję kupna", "disable_sell": "Wyłącz akcję sprzedaży", + "cake_2fa_preset" : "Ciasto 2FA Preset", + "narrow": "Wąski", + "normal": "Normalna", + "aggressive": "Nadgorliwy", + "require_for_assessing_wallet": "Wymagaj dostępu do portfela", + "require_for_sends_to_non_contacts" : "Wymagaj wysyłania do osób niekontaktowych", + "require_for_sends_to_contacts" : "Wymagaj wysyłania do kontaktów", + "require_for_sends_to_internal_wallets" : "Wymagaj wysyłania do portfeli wewnętrznych", + "require_for_exchanges_to_internal_wallets" : "Wymagaj wymiany do portfeli wewnętrznych", + "require_for_adding_contacts" : "Wymagane do dodania kontaktów", + "require_for_creating_new_wallets" : "Wymagane do tworzenia nowych portfeli", + "require_for_all_security_and_backup_settings" : "Wymagaj dla wszystkich ustawień zabezpieczeń i kopii zapasowych", "available_balance_description": "Dostępne saldo jest równoważne z saldem portfela minus zamrożone saldo.", "syncing_wallet_alert_title": "Twój portfel się synchronizuje", "syncing_wallet_alert_content": "Twoje saldo i lista transakcji mogą nie być kompletne, dopóki u góry nie pojawi się napis „SYNCHRONIZOWANY”. Kliknij/stuknij, aby dowiedzieć się więcej.", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 8af35a20e..d79d722aa 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -631,6 +631,18 @@ "setup_totp_recommended": "Configurar TOTP (recomendado)", "disable_buy": "Desativar ação de compra", "disable_sell": "Desativar ação de venda", + "cake_2fa_preset" : "Predefinição de bolo 2FA", + "narrow": "Estreito", + "normal": "Normal", + "aggressive": "excessivamente zeloso", + "require_for_assessing_wallet": "Requer para acessar a carteira", + "require_for_sends_to_non_contacts" : "Exigir para envios para não-contatos", + "require_for_sends_to_contacts" : "Exigir para envios para contatos", + "require_for_sends_to_internal_wallets" : "Exigir envios para carteiras internas", + "require_for_exchanges_to_internal_wallets" : "Requer trocas para carteiras internas", + "require_for_adding_contacts" : "Requer para adicionar contatos", + "require_for_creating_new_wallets" : "Requer para criar novas carteiras", + "require_for_all_security_and_backup_settings" : "Exigir todas as configurações de segurança e backup", "available_balance_description": "Seu saldo disponível é o saldo total menos o saldo congelado. O saldo congelado é o saldo que você não pode gastar, mas que ainda não foi confirmado na blockchain. O saldo congelado é geralmente o resultado de transações recentes.", "syncing_wallet_alert_title": "Sua carteira está sincronizando", "syncing_wallet_alert_content": "Seu saldo e lista de transações podem não estar completos até que diga “SYNCHRONIZED” no topo. Clique/toque para saber mais.", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 03e0fbbd5..3136bd3fb 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -633,6 +633,18 @@ "setup_totp_recommended": "Настроить TOTP (рекомендуется)", "disable_buy": "Отключить действие покупки", "disable_sell": "Отключить действие продажи", + "cake_2fa_preset" : "Торт 2FA Preset", + "narrow": "Узкий", + "normal": "Нормальный", + "aggressive": "чрезмерно усердный", + "require_for_assessing_wallet": "Требовать для доступа к кошельку", + "require_for_sends_to_non_contacts" : "Требовать для отправки не контактам", + "require_for_sends_to_contacts" : "Требовать для отправки контактам", + "require_for_sends_to_internal_wallets" : "Требовать отправки на внутренние кошельки", + "require_for_exchanges_to_internal_wallets" : "Требовать для обмена на внутренние кошельки", + "require_for_adding_contacts" : "Требовать добавления контактов", + "require_for_creating_new_wallets" : "Требовать для создания новых кошельков", + "require_for_all_security_and_backup_settings" : "Требовать все настройки безопасности и резервного копирования", "available_balance_description": "Доступный баланс - это средства, которые вы можете использовать для покупки или продажи криптовалюты.", "syncing_wallet_alert_title": "Ваш кошелек синхронизируется", "syncing_wallet_alert_content": "Ваш баланс и список транзакций могут быть неполными, пока вверху не будет написано «СИНХРОНИЗИРОВАНО». Щелкните/коснитесь, чтобы узнать больше.", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index bb2fcd17e..b1ab7c42a 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "ตั้งค่า TOTP (แนะนำ)", "disable_buy": "ปิดการใช้งานการซื้อ", "disable_sell": "ปิดการใช้งานการขาย", + "cake_2fa_preset" : "เค้ก 2FA ที่ตั้งไว้ล่วงหน้า", + "narrow": "แคบ", + "normal": "ปกติ", + "aggressive": "กระตือรือร้นมากเกินไป", + "require_for_assessing_wallet": "จำเป็นสำหรับการเข้าถึงกระเป๋าเงิน", + "require_for_sends_to_non_contacts" : "จำเป็นต้องส่งไปยังผู้ที่ไม่ได้ติดต่อ", + "require_for_sends_to_contacts" : "จำเป็นต้องส่งไปยังผู้ติดต่อ", + "require_for_sends_to_internal_wallets" : "จำเป็นต้องส่งไปยังกระเป๋าเงินภายใน", + "require_for_exchanges_to_internal_wallets" : "ต้องการการแลกเปลี่ยนไปยังกระเป๋าเงินภายใน", + "require_for_adding_contacts" : "ต้องการสำหรับการเพิ่มผู้ติดต่อ", + "require_for_creating_new_wallets" : "จำเป็นสำหรับการสร้างกระเป๋าเงินใหม่", + "require_for_all_security_and_backup_settings" : "จำเป็นสำหรับการตั้งค่าความปลอดภัยและการสำรองข้อมูลทั้งหมด", "available_balance_description": "จำนวนเงินที่คุณสามารถใช้ได้ในการซื้อหรือขาย", "syncing_wallet_alert_title": "กระเป๋าสตางค์ของคุณกำลังซิงค์", "syncing_wallet_alert_content": "รายการยอดเงินและธุรกรรมของคุณอาจไม่สมบูรณ์จนกว่าจะมีข้อความว่า “ซิงโครไนซ์” ที่ด้านบน คลิก/แตะเพื่อเรียนรู้เพิ่มเติม่", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 88eec0934..1f956d9bd 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -631,6 +631,19 @@ "setup_2fa_text": "Cake 2FA, soğuk hava deposu kadar güvenli DEĞİLDİR. 2FA, siz uyurken arkadaşınızın parmak izinizi sağlaması gibi temel saldırı türlerine karşı koruma sağlar.\n\n Cake 2FA, gelişmiş bir saldırgan tarafından güvenliği ihlal edilmiş bir cihaza karşı koruma SAĞLAMAZ.\n\n 2FA kodlarınıza erişimi kaybederseniz , BU CÜZDANA ERİŞİMİNİZİ KAYBEDECEKSİNİZ. Mnemonic seed'den cüzdanınızı geri yüklemeniz gerekecek. BU NEDENLE HATIRLAYICI TOHUMLARINIZI YEDEKLEMELİSİNİZ! Ayrıca anımsatıcı tohumlarınıza erişimi olan biri, Cake 2FA'yı atlayarak paranızı çalabilir.\n\n Cake, anımsatıcı tohumlarınıza erişimi kaybederseniz size yardımcı olamaz, çünkü Cake bir saklama dışı cüzdan.", "setup_totp_recommended": "TOTP'yi kurun (Önerilir)", "disable_buy": "Satın alma işlemini devre dışı bırak", + "disable_sell": "Satış işlemini devre dışı bırak", + "cake_2fa_preset" : "Kek 2FA Ön Ayarı", + "narrow": "Dar", + "normal": "Normal", + "aggressive": "Aşırı duyarlı", + "require_for_assessing_wallet": "Cüzdana erişmek için gerekli", + "require_for_sends_to_non_contacts" : "Kişi olmayan kişilere göndermeler için gerekli kıl", + "require_for_sends_to_contacts" : "Kişilere göndermeler için gerekli kıl", + "require_for_sends_to_internal_wallets" : "Dahili cüzdanlara yapılan gönderimler için gereklilik", + "require_for_exchanges_to_internal_wallets" : "Dahili cüzdanlara değişim gerektir", + "require_for_adding_contacts" : "Kişi eklemek için gerekli", + "require_for_creating_new_wallets" : "Yeni cüzdan oluşturmak için gerekli", + "require_for_all_security_and_backup_settings" : "Tüm güvenlik ve yedekleme ayarları için iste", "disable_sell": "Satış işlemini devre dışı bırak", "available_balance_description": "Bu, cüzdanınızda harcayabileceğiniz miktar. Bu miktar, cüzdanınızdan çekilebilecek toplam bakiyeden daha düşük olabilir, çünkü bazı fonlar henüz kullanılamaz durumda olabilir.", "syncing_wallet_alert_title": "Cüzdanınız senkronize ediliyor", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 400d13caa..90e00efb9 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -632,6 +632,18 @@ "setup_totp_recommended": "Налаштувати TOTP (рекомендовано)", "disable_buy": "Вимкнути дію покупки", "disable_sell": "Вимкнути дію продажу", + "cake_2fa_preset" : "Торт 2FA Preset", + "narrow": "вузькі", + "normal": "нормальний", + "aggressive": "Надто старанний", + "require_for_assessing_wallet": "Потрібен доступ до гаманця", + "require_for_sends_to_non_contacts" : "Вимагати для надсилання неконтактним особам", + "require_for_sends_to_contacts" : "Вимагати для надсилання контактам", + "require_for_sends_to_internal_wallets" : "Вимагати надсилання на внутрішні гаманці", + "require_for_exchanges_to_internal_wallets" : "Вимагати обміну на внутрішні гаманці", + "require_for_adding_contacts" : "Потрібен для додавання контактів", + "require_for_creating_new_wallets" : "Потрібно для створення нових гаманців", + "require_for_all_security_and_backup_settings" : "Вимагати всіх налаштувань безпеки та резервного копіювання", "available_balance_description": "Це сума, яку ви можете витратити, не включаючи невизначені кошти. Це може бути менше, ніж загальний баланс, якщо ви витратили кошти, які ще не підтверджені.", "syncing_wallet_alert_title": "Ваш гаманець синхронізується", "syncing_wallet_alert_content": "Ваш баланс та список транзакцій може бути неповним, доки вгорі не буде написано «СИНХРОНІЗОВАНО». Натисніть/торкніться, щоб дізнатися більше.", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 9ccbb6964..89c7cfbde 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -626,6 +626,18 @@ "setup_totp_recommended": "TOTP ترتیب دیں (تجویز کردہ)", "disable_buy": "خرید ایکشن کو غیر فعال کریں۔", "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔", + "cake_2fa_preset" : "کیک 2FA پیش سیٹ", + "narrow": "تنگ", + "normal": "نارمل", + "aggressive": "حد سے زیادہ پرجوش", + "require_for_assessing_wallet": "بٹوے تک رسائی کے لیے درکار ہے۔", + "require_for_sends_to_non_contacts" : "غیر رابطوں کو بھیجنے کی ضرورت ہے۔", + "require_for_sends_to_contacts" : "رابطوں کو بھیجنے کی ضرورت ہے۔", + "require_for_sends_to_internal_wallets" : "اندرونی بٹوے پر بھیجنے کے لیے درکار ہے۔", + "require_for_exchanges_to_internal_wallets" : "اندرونی بٹوے میں تبادلے کی ضرورت ہے۔", + "require_for_adding_contacts" : "رابطوں کو شامل کرنے کی ضرورت ہے۔", + "require_for_creating_new_wallets" : "نئے بٹوے بنانے کی ضرورت ہے۔", + "require_for_all_security_and_backup_settings" : "تمام سیکورٹی اور بیک اپ کی ترتیبات کے لیے درکار ہے۔", "available_balance_description": "”دستیاب بیلنس” یا ”تصدیق شدہ بیلنس” وہ فنڈز ہیں جو فوری طور پر خرچ کیے جا سکتے ہیں۔ اگر فنڈز کم بیلنس میں ظاہر ہوتے ہیں لیکن اوپر کے بیلنس میں نہیں، تو آپ کو مزید نیٹ ورک کی تصدیقات حاصل کرنے کے لیے آنے والے فنڈز کے لیے چند منٹ انتظار کرنا چاہیے۔ مزید تصدیق حاصل کرنے کے بعد، وہ قابل خرچ ہوں گے۔", "syncing_wallet_alert_title": "آپ کا بٹوہ مطابقت پذیر ہو رہا ہے۔", "syncing_wallet_alert_content": "آپ کے بیلنس اور لین دین کی فہرست اس وقت تک مکمل نہیں ہو سکتی جب تک کہ یہ سب سے اوپر \"SYNCRONIZED\" نہ کہے۔ مزید جاننے کے لیے کلک/تھپتھپائیں۔", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 85084943b..3c178430c 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -628,6 +628,18 @@ "setup_totp_recommended": "Sọ TOTP (Kẹṣọdọ)", "disable_buy": "Ko iṣọrọ ọja", "disable_sell": "Ko iṣọrọ iṣọrọ", + "cake_2fa_preset" : "Cake 2FA Tito", + "narrow": "Taara", + "normal": "Deede", + "aggressive": "Onítara", + "require_for_assessing_wallet": "Beere fun wiwọle si apamọwọ", + "require_for_sends_to_non_contacts" : "Beere fun fifiranṣẹ si awọn ti kii ṣe awọn olubasọrọ", + "require_for_sends_to_contacts" : "Beere fun fifiranṣẹ si awọn olubasọrọ", + "require_for_sends_to_internal_wallets" : "Beere fun fifiranṣẹ si awọn apamọwọ inu", + "require_for_exchanges_to_internal_wallets" : "Beere fun awọn paṣipaarọ si awọn apamọwọ inu", + "require_for_adding_contacts" : "Beere fun fifi awọn olubasọrọ kun", + "require_for_creating_new_wallets" : "Beere fun ṣiṣẹda titun Woleti", + "require_for_all_security_and_backup_settings" : "Beere fun gbogbo aabo ati awọn eto afẹyinti", "available_balance_description": "“Iwọntunwọnsi Wa” tabi “Iwọntunwọnsi Ijẹrisi” jẹ awọn owo ti o le ṣee lo lẹsẹkẹsẹ. Ti awọn owo ba han ni iwọntunwọnsi kekere ṣugbọn kii ṣe iwọntunwọnsi oke, lẹhinna o gbọdọ duro iṣẹju diẹ fun awọn owo ti nwọle lati gba awọn ijẹrisi nẹtiwọọki diẹ sii. Lẹhin ti wọn gba awọn ijẹrisi diẹ sii, wọn yoo jẹ inawo.", "syncing_wallet_alert_title": "Apamọwọ rẹ n muṣiṣẹpọ", "syncing_wallet_alert_content": "Iwontunws.funfun rẹ ati atokọ idunadura le ma pari titi ti yoo fi sọ “SYNCHRONIZED” ni oke. Tẹ/tẹ ni kia kia lati ni imọ siwaju sii.", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 02213c66d..6866f4068 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -631,6 +631,18 @@ "setup_totp_recommended": "设置 TOTP(推荐)", "disable_buy": "禁用购买操作", "disable_sell": "禁用卖出操作", + "cake_2fa_preset" : "蛋糕 2FA 预设", + "narrow": "狭窄的", + "normal": "普通的", + "aggressive": "过分热心", + "require_for_assessing_wallet": "需要访问钱包", + "require_for_sends_to_non_contacts" : "需要发送给非联系人", + "require_for_sends_to_contacts" : "需要发送给联系人", + "require_for_sends_to_internal_wallets" : "需要发送到内部钱包", + "require_for_exchanges_to_internal_wallets" : "需要兑换到内部钱包", + "require_for_adding_contacts" : "需要添加联系人", + "require_for_creating_new_wallets" : "创建新钱包的要求", + "require_for_all_security_and_backup_settings" : "需要所有安全和备份设置", "available_balance_description": "可用余额是您可以使用的金额。冻结余额是您当前正在等待确认的金额。", "syncing_wallet_alert_title": "您的钱包正在同步", "syncing_wallet_alert_content": "您的余额和交易列表可能不完整,直到顶部显示“已同步”。单击/点击以了解更多信息。", From 3ce4000dcf47fa99053cf7beb52d456cb4feae9e Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 4 Aug 2023 20:01:49 +0300 Subject: [PATCH 4/5] Cw 78 ethereum (#862) * Add initial flow for ethereum * Add initial create Eth wallet flow * Complete Ethereum wallet creation flow * Fix web3dart versioning issue * Add primary receive address extracted from private key * Implement open wallet functionality * Implement restore wallet from seed functionality * Fixate web3dart version as higher versions cause some issues * Add Initial Transaction priorities for eth Add estimated gas price * Rename priority value to tip * Re-order wallet types * Change ethereum node Fix connection issues * Fix estimating gas for priority * Add case for ethereum to fetch it's seeds * Add case for ethereum to request node * Fix Exchange screen initial pairs * Add initial send transaction flow * Add missing configure for ethereum class * Add Eth address initial setup * Fix Private key for Ethereum wallets * Change sign/send transaction flow * - Fix Conflicts with main - Remove unused function from Haven configure.dart * Add build command for ethereum package * Add missing Node list file to pubspec * - Fix balance display - Fix parsing of Ethereum amount - Add more Ethereum Nodes * - Fix extracting Ethereum Private key from seeds - Integrate signing/sending transaction with the send view model * - Update and Fix Conflicts with main * Add Balances for ERC20 tokens * Fix conflicts with main * Add erc20 abi json * Add send erc20 tokens initial function * add missing getHeightByDate in Haven * Allow contacts and wallets from the same tag * Add Shiba Inu icon * Add send ERC-20 tokens initial flow * Add missing import in generated file * Add initial approach for transaction sending for ERC-20 tokens * Refactor signing/sending transactions * Add initial flow for transactions subscription * Refactor signing/sending transactions * Add home settings icon * Fix conflicts with main * Initial flow for home settings * Add logic flow for adding erc20 tokens * Fix initial UI * Finalize UI for Tokens * Integrate UI with Ethereum flow * Add "Enable/Disable" feature for ERC20 tokens * Add initial Erc20 tokens * Add Sorting and Pin Native Token features * Fix price sorting * Sort tokens list as well when Sort criteria changes * - Improve sorting balances flow - Add initial add token from search bar flow * Fix Accounts Popup UI * Fix Pin native token * Fix Enabling/Disabling tokens Fix sorting by fiat once app is opened Improve token availability mechanism * Fix deleting token Fix renaming tokens * Fix issue with search * Add more tokens * - Fix scroll issue - Add ERC20 tokens placeholder image in picker * - Separate and organize default erc20 tokens - Fix scrolling - Add token placeholder images in picker - Sort disabled tokens alphabetically * Change BNB token initial availability * Fix Conflicts with main * Fix Conflicts with main * Add Verse ERC20 token to the initial tokens list * Add rename wallet to Ethereum * Integrate EtherScan API for fetching address transactions Generate Ethereum specific secrets in Ethereum package * Adjust transactions fiat price for ERC20 tokens * Free Up GitHub Actions Ubuntu Runner Disk Space * Free Up GitHub Actions Ubuntu Runner Disk space (trial 2) * Fix Transaction Fee display * Save transaction history * Enhance loading time for erc20 tokens transactions * Minor Fixes and Enhancements * Fix sending erc20 fix block explorer issue * Fix int overflow * Fix transaction amount conversions * Minor: `slow` -> `Slow` * Update build guide * Fix fetching fiat rate taking a lot of time by only fetching enabled tokens only and making the API calls in parallel not sequential * Update transactions on a periodic basis * For fee, use ETH spot price, not ERC-20 spot price * Add Etherscan History privacy option to enable/disable Etherscan API * Show estimated fee amounts in the send screen * fix send fiat fields parsing issue * Fix transactions estimated fee less than actual fee * handle balance sorting when balance is disabled Handle empty transactions list * Fix Delete Ethereum wallet Fix balance < 0.01 * Fix Decimal place for Ethereum amount Fix sending amount issue * Change words count * Remove balance hint and Full balance row from Ethereum wallets * support changing the asset type in send templates * Fix Templates for ERC tokens issues * Fix conflicts in send templates * Disable batch sending in Ethereum * Fix Fee calculation with different priorities * Fix Conflicts with main * Add offline error to ignored exceptions --------- Co-authored-by: Justin Ehrenhofer --- .github/workflows/pr_test_build.yml | 2 + .gitignore | 3 + assets/ethereum_server_list.yml | 10 + assets/images/home_screen_settings_icon.png | Bin 0 -> 394 bytes configure_cake_wallet_android.sh | 10 + .../lib/electrum_transaction_history.dart | 3 +- cw_bitcoin/lib/electrum_transaction_info.dart | 7 +- cw_bitcoin/lib/electrum_wallet.dart | 1 + cw_bitcoin/pubspec.yaml | 2 +- cw_core/lib/currency_for_wallet_type.dart | 2 + cw_core/lib/erc20_token.dart | 64 + cw_core/lib/node.dart | 19 +- cw_core/lib/wallet_base.dart | 2 + cw_core/lib/wallet_service.dart | 2 +- cw_core/lib/wallet_type.dart | 16 +- cw_ethereum/.gitignore | 30 + cw_ethereum/.metadata | 10 + cw_ethereum/CHANGELOG.md | 3 + cw_ethereum/LICENSE | 1 + cw_ethereum/README.md | 39 + cw_ethereum/analysis_options.yaml | 4 + cw_ethereum/lib/cw_ethereum.dart | 7 + cw_ethereum/lib/default_erc20_tokens.dart | 302 +++ cw_ethereum/lib/erc20_balance.dart | 47 + cw_ethereum/lib/ethereum_client.dart | 230 ++ cw_ethereum/lib/ethereum_exceptions.dart | 11 + cw_ethereum/lib/ethereum_formatter.dart | 25 + cw_ethereum/lib/ethereum_mnemonics.dart | 2058 +++++++++++++++++ .../lib/ethereum_transaction_credentials.dart | 17 + .../lib/ethereum_transaction_history.dart | 77 + .../lib/ethereum_transaction_info.dart | 74 + .../lib/ethereum_transaction_model.dart | 47 + .../lib/ethereum_transaction_priority.dart | 52 + cw_ethereum/lib/ethereum_wallet.dart | 473 ++++ .../lib/ethereum_wallet_addresses.dart | 33 + .../ethereum_wallet_creation_credentials.dart | 23 + cw_ethereum/lib/ethereum_wallet_service.dart | 108 + cw_ethereum/lib/file.dart | 39 + .../lib/pending_ethereum_transaction.dart | 36 + cw_ethereum/pubspec.yaml | 68 + cw_ethereum/test/cw_ethereum_test.dart | 12 + cw_haven/lib/haven_wallet.dart | 1 + cw_monero/lib/monero_transaction_info.dart | 13 +- cw_monero/lib/monero_wallet.dart | 1 + howto-build-android.md | 12 +- lib/bitcoin/cw_bitcoin.dart | 2 +- lib/core/address_validator.dart | 23 +- lib/core/backup_service.dart | 18 + lib/core/fiat_conversion_service.dart | 17 +- lib/core/seed_validator.dart | 3 + lib/di.dart | 33 +- lib/entities/default_settings_migration.dart | 46 +- lib/entities/main_actions.dart | 2 + lib/entities/node_list.dart | 16 + lib/entities/preferences_key.dart | 5 + lib/entities/priority_for_wallet_type.dart | 3 + lib/entities/sort_balance_types.dart | 19 + lib/entities/template.dart | 2 +- lib/ethereum/cw_ethereum.dart | 126 + lib/main.dart | 2 +- lib/reactions/fiat_rate_update.dart | 15 + lib/reactions/on_current_wallet_change.dart | 16 +- lib/router.dart | 37 +- lib/routes.dart | 2 + .../desktop_wallet_selection_dropdown.dart | 3 + .../screens/dashboard/edit_token_page.dart | 309 +++ .../screens/dashboard/home_settings_page.dart | 164 ++ .../dashboard/widgets/address_page.dart | 152 +- .../dashboard/widgets/balance_page.dart | 376 +-- .../dashboard/widgets/menu_widget.dart | 34 +- .../exchange/widgets/exchange_card.dart | 21 +- .../new_wallet/new_wallet_type_page.dart | 7 +- .../restore/restore_wallet_options_page.dart | 85 - lib/src/screens/seed/pre_seed_page.dart | 15 +- lib/src/screens/send/send_page.dart | 202 +- lib/src/screens/send/send_template_page.dart | 71 +- .../widgets/prefix_currency_icon_widget.dart | 54 +- lib/src/screens/send/widgets/send_card.dart | 749 +++--- .../send/widgets/send_template_card.dart | 277 ++- lib/src/screens/settings/privacy_page.dart | 10 +- .../widgets/settings_switcher_cell.dart | 19 +- .../screens/wallet_list/wallet_list_page.dart | 3 + lib/src/widgets/checkbox_widget.dart | 67 +- lib/src/widgets/picker.dart | 93 +- lib/src/widgets/standard_list.dart | 11 +- lib/store/settings_store.dart | 81 +- lib/utils/exception_handler.dart | 1 + .../contact_list/contact_list_view_model.dart | 9 +- .../dashboard/balance_view_model.dart | 68 +- .../dashboard/home_settings_view_model.dart | 121 + .../dashboard/transaction_list_item.dart | 8 + .../exchange/exchange_view_model.dart | 4 + .../node_list/node_list_view_model.dart | 3 + lib/view_model/send/output.dart | 14 +- .../send/send_template_view_model.dart | 20 +- lib/view_model/send/send_view_model.dart | 49 +- lib/view_model/send/template_view_model.dart | 34 +- .../settings/privacy_settings_view_model.dart | 19 +- .../transaction_details_view_model.dart | 206 +- .../wallet_address_list_view_model.dart | 31 + lib/view_model/wallet_keys_view_model.dart | 8 +- lib/view_model/wallet_new_vm.dart | 3 + lib/view_model/wallet_restore_view_model.dart | 6 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 - model_generator.sh | 1 + pubspec_base.yaml | 1 + res/values/strings_ar.arb | 28 +- res/values/strings_bg.arb | 28 +- res/values/strings_cs.arb | 28 +- res/values/strings_de.arb | 28 +- res/values/strings_en.arb | 28 +- res/values/strings_es.arb | 28 +- res/values/strings_fr.arb | 28 +- res/values/strings_ha.arb | 28 +- res/values/strings_hi.arb | 28 +- res/values/strings_hr.arb | 28 +- res/values/strings_id.arb | 28 +- res/values/strings_it.arb | 28 +- res/values/strings_ja.arb | 28 +- res/values/strings_ko.arb | 28 +- res/values/strings_my.arb | 28 +- res/values/strings_nl.arb | 28 +- res/values/strings_pl.arb | 28 +- res/values/strings_pt.arb | 28 +- res/values/strings_ru.arb | 28 +- res/values/strings_th.arb | 28 +- res/values/strings_tr.arb | 28 +- res/values/strings_uk.arb | 28 +- res/values/strings_ur.arb | 28 +- res/values/strings_yo.arb | 28 +- res/values/strings_zh.arb | 28 +- scripts/android/pubspec_gen.sh | 2 +- scripts/ios/app_config.sh | 2 +- tool/configure.dart | 112 +- tool/generate_secrets_config.dart | 20 +- tool/import_secrets_config.dart | 22 +- tool/utils/secret_key.dart | 4 + 137 files changed, 7164 insertions(+), 1492 deletions(-) create mode 100644 assets/ethereum_server_list.yml create mode 100644 assets/images/home_screen_settings_icon.png create mode 100644 configure_cake_wallet_android.sh create mode 100644 cw_core/lib/erc20_token.dart create mode 100644 cw_ethereum/.gitignore create mode 100644 cw_ethereum/.metadata create mode 100644 cw_ethereum/CHANGELOG.md create mode 100644 cw_ethereum/LICENSE create mode 100644 cw_ethereum/README.md create mode 100644 cw_ethereum/analysis_options.yaml create mode 100644 cw_ethereum/lib/cw_ethereum.dart create mode 100644 cw_ethereum/lib/default_erc20_tokens.dart create mode 100644 cw_ethereum/lib/erc20_balance.dart create mode 100644 cw_ethereum/lib/ethereum_client.dart create mode 100644 cw_ethereum/lib/ethereum_exceptions.dart create mode 100644 cw_ethereum/lib/ethereum_formatter.dart create mode 100644 cw_ethereum/lib/ethereum_mnemonics.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_credentials.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_history.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_info.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_model.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_priority.dart create mode 100644 cw_ethereum/lib/ethereum_wallet.dart create mode 100644 cw_ethereum/lib/ethereum_wallet_addresses.dart create mode 100644 cw_ethereum/lib/ethereum_wallet_creation_credentials.dart create mode 100644 cw_ethereum/lib/ethereum_wallet_service.dart create mode 100644 cw_ethereum/lib/file.dart create mode 100644 cw_ethereum/lib/pending_ethereum_transaction.dart create mode 100644 cw_ethereum/pubspec.yaml create mode 100644 cw_ethereum/test/cw_ethereum_test.dart create mode 100644 lib/entities/sort_balance_types.dart create mode 100644 lib/ethereum/cw_ethereum.dart create mode 100644 lib/src/screens/dashboard/edit_token_page.dart create mode 100644 lib/src/screens/dashboard/home_settings_page.dart delete mode 100644 lib/src/screens/restore/restore_wallet_options_page.dart create mode 100644 lib/view_model/dashboard/home_settings_view_model.dart diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 16b036344..de8396d59 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -92,6 +92,7 @@ jobs: cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs - name: Add secrets @@ -124,6 +125,7 @@ jobs: 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 etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart - name: Rename app run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties diff --git a/.gitignore b/.gitignore index 70d99f753..6fd8f33d6 100644 --- a/.gitignore +++ b/.gitignore @@ -90,7 +90,9 @@ android/key.properties **/tool/.secrets-prod.json **/tool/.secrets-test.json **/tool/.secrets-config.json +**/tool/.ethereum-secrets-config.json **/lib/.secrets.g.dart +**/cw_ethereum/lib/.secrets.g.dart vendor/ @@ -121,6 +123,7 @@ cw_haven/android/.cxx/ lib/bitcoin/bitcoin.dart lib/monero/monero.dart lib/haven/haven.dart +lib/ethereum/ethereum.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png diff --git a/assets/ethereum_server_list.yml b/assets/ethereum_server_list.yml new file mode 100644 index 000000000..125085d88 --- /dev/null +++ b/assets/ethereum_server_list.yml @@ -0,0 +1,10 @@ +- + uri: ethereum.publicnode.com +- + uri: eth.llamarpc.com +- + uri: rpc.flashbots.net +- + uri: eth-mainnet.public.blastapi.io +- + uri: ethereum.publicnode.com \ No newline at end of file diff --git a/assets/images/home_screen_settings_icon.png b/assets/images/home_screen_settings_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6c750f5f678d5e34224c36c6e342b5bd1fc7096c GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O(A@}4e^Ar*{ILvjTV8SwbN7iegC#K5_=fysf< zfNA{}w<+Vy~nwu6} zurhn~!14%>Ou&jQx86?;dw4W#JNvHwGq#_StKF~p9#l0|UOpk9joCPLyHY8O<>Qc?Q|0-bP0l+XkK=AfE8 literal 0 HcmV?d00001 diff --git a/configure_cake_wallet_android.sh b/configure_cake_wallet_android.sh new file mode 100644 index 000000000..b80ebc46e --- /dev/null +++ b/configure_cake_wallet_android.sh @@ -0,0 +1,10 @@ +cd scripts/android +source ./app_env.sh cakewallet +./app_config.sh +cd ../.. && flutter pub get +cd cw_core && 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 .. +cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 553795470..be039fa36 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_bitcoin/file.dart'; @@ -67,7 +66,7 @@ abstract class ElectrumTransactionHistoryBase Future _load() async { try { final content = await _read(); - final txs = content['transactions'] as Map ?? {}; + final txs = content['transactions'] as Map? ?? {}; txs.entries.forEach((entry) { final val = entry.value; diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index b034c06b1..bf5ec2c4f 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,4 +1,3 @@ -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:cw_bitcoin/address_from_output.dart'; @@ -217,9 +216,9 @@ class ElectrumTransactionInfo extends TransactionInfo { height: info.height, amount: info.amount, fee: info.fee, - direction: direction ?? info.direction, - date: date ?? info.date, - isPending: isPending ?? info.isPending, + direction: direction, + date: date, + isPending: isPending, confirmations: info.confirmations); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index aadf87572..f9437e668 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -431,6 +431,7 @@ abstract class ElectrumWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); final currentWalletFile = File(currentWalletPath); diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 455ceb4a7..481a41ac5 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: unorm_dart: ^0.2.0 cryptography: ^2.0.5 encrypt: ^5.0.1 - + dev_dependencies: flutter_test: sdk: flutter diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 3904fc049..8ac8c1fc6 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -11,6 +11,8 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart new file mode 100644 index 000000000..2e205e484 --- /dev/null +++ b/cw_core/lib/erc20_token.dart @@ -0,0 +1,64 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:hive/hive.dart'; + +part 'erc20_token.g.dart'; + +@HiveType(typeId: Erc20Token.typeId) +class Erc20Token extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String name; + @HiveField(1) + final String symbol; + @HiveField(2) + final String contractAddress; + @HiveField(3) + final int decimal; + @HiveField(4, defaultValue: true) + bool _enabled; + @HiveField(5) + final String? iconPath; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + Erc20Token({ + required this.name, + required this.symbol, + required this.contractAddress, + required this.decimal, + bool enabled = true, + this.iconPath, + }) : _enabled = enabled, + super( + name: symbol.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: "ETH", + iconPath: iconPath, + ); + + Erc20Token.copyWith(Erc20Token other, String? icon) + : this.name = other.name, + this.symbol = other.symbol, + this.contractAddress = other.contractAddress, + this.decimal = other.decimal, + this._enabled = other.enabled, + this.iconPath = icon, + super( + name: other.name, + title: other.symbol.toUpperCase(), + fullName: other.name, + tag: "ETH", + iconPath: icon, + ); + + static const typeId = 12; + static const boxName = 'Erc20Tokens'; + + @override + bool operator ==(other) => other is Erc20Token && other.contractAddress == contractAddress; + + @override + int get hashCode => contractAddress.hashCode; +} diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 0848e8d94..3fa45b44c 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -75,6 +75,8 @@ class Node extends HiveObject with Keyable { return createUriFromElectrumAddress(uriRaw); case WalletType.haven: return Uri.http(uriRaw, ''); + case WalletType.ethereum: + return Uri.https(uriRaw, ''); default: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -124,6 +126,8 @@ class Node extends HiveObject with Keyable { return requestElectrumServer(); case WalletType.haven: return requestMoneroNode(); + case WalletType.ethereum: + return requestElectrumServer(); default: return false; } @@ -166,7 +170,7 @@ class Node extends HiveObject with Keyable { } catch (_) { return false; } -} + } Future requestNodeWithProxy(String proxy) async { @@ -193,4 +197,17 @@ class Node extends HiveObject with Keyable { return false; } } + + Future requestEthereumServer() async { + try { + final response = await http.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + return response.statusCode >= 200 && response.statusCode < 300; + } catch (_) { + return false; + } + } } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index e5f84f467..019f87631 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -75,4 +75,6 @@ abstract class WalletBase< Future? updateBalance(); void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null; + + Future renameWalletFiles(String newWalletName); } diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index 5e216e225..f95bc1a44 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -18,5 +18,5 @@ abstract class WalletService remove(String wallet); - Future rename(String name, String password, String newName); + Future rename(String currentName, String password, String newName); } diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 61a571fcf..a65839041 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -7,7 +7,8 @@ const walletTypes = [ WalletType.monero, WalletType.bitcoin, WalletType.litecoin, - WalletType.haven + WalletType.haven, + WalletType.ethereum, ]; const walletTypeTypeId = 5; @@ -27,6 +28,9 @@ enum WalletType { @HiveField(4) haven, + + @HiveField(5) + ethereum, } int serializeToInt(WalletType type) { @@ -39,6 +43,8 @@ int serializeToInt(WalletType type) { return 2; case WalletType.haven: return 3; + case WalletType.ethereum: + return 4; default: return -1; } @@ -54,6 +60,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.litecoin; case 3: return WalletType.haven; + case 4: + return WalletType.ethereum; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -69,6 +77,8 @@ String walletTypeToString(WalletType type) { return 'Litecoin'; case WalletType.haven: return 'Haven'; + case WalletType.ethereum: + return 'Ethereum'; default: return ''; } @@ -84,6 +94,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Litecoin (LTC)'; case WalletType.haven: return 'Haven (XHV)'; + case WalletType.ethereum: + return 'Ethereum (ETH)'; default: return ''; } @@ -99,6 +111,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } diff --git a/cw_ethereum/.gitignore b/cw_ethereum/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_ethereum/.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_ethereum/.metadata b/cw_ethereum/.metadata new file mode 100644 index 000000000..1e05dac7f --- /dev/null +++ b/cw_ethereum/.metadata @@ -0,0 +1,10 @@ +# 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 and should not be manually edited. + +version: + revision: eb6d86ee27deecba4a83536aa20f366a6044895c + channel: stable + +project_type: package diff --git a/cw_ethereum/CHANGELOG.md b/cw_ethereum/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_ethereum/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_ethereum/LICENSE b/cw_ethereum/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_ethereum/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_ethereum/README.md b/cw_ethereum/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_ethereum/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_ethereum/analysis_options.yaml b/cw_ethereum/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_ethereum/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_ethereum/lib/cw_ethereum.dart b/cw_ethereum/lib/cw_ethereum.dart new file mode 100644 index 000000000..af9ea7ee0 --- /dev/null +++ b/cw_ethereum/lib/cw_ethereum.dart @@ -0,0 +1,7 @@ +library cw_ethereum; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_ethereum/lib/default_erc20_tokens.dart b/cw_ethereum/lib/default_erc20_tokens.dart new file mode 100644 index 000000000..241e301ce --- /dev/null +++ b/cw_ethereum/lib/default_erc20_tokens.dart @@ -0,0 +1,302 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultErc20Tokens { + final List _defaultTokens = [ + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USDT Tether", + symbol: "USDT", + contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Dai", + symbol: "DAI", + contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Pepe", + symbol: "PEPE", + contractAddress: "0x6982508145454ce325ddbe47a25d4ec3d2311933", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "SHIBA INU", + symbol: "SHIB", + contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ApeCoin", + symbol: "APE", + contractAddress: "0x4d224452801aced8b2f0aebe155379bb5d594381", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Matic Token", + symbol: "MATIC", + contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Gitcoin", + symbol: "GTC", + contractAddress: "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Compound", + symbol: "COMP", + contractAddress: "0xc00e94cb662c3520282e6f5717214004a7f26888", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Aave Token", + symbol: "AAVE", + contractAddress: "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Uniswap", + symbol: "UNI", + contractAddress: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Decentraland", + symbol: "MANA", + contractAddress: "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Storj", + symbol: "STORJ", + contractAddress: "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Maker", + symbol: "MKR", + contractAddress: "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Orchid", + symbol: "OXT", + contractAddress: "0x4575f41308EC1483f3d399aa9a2826d74Da13Deb", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Paxos Gold", + symbol: "PAXG", + contractAddress: "0x45804880De22913dAFE09f4980848ECE6EcbAf78", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Binance Coin", + symbol: "BNB", + contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "stETH", + symbol: "stETH", + contractAddress: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Lido DAO", + symbol: "LDO", + contractAddress: "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Arbitrum", + symbol: "ARB", + contractAddress: "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Graph Token", + symbol: "GRT", + contractAddress: "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Frax", + symbol: "FRAX", + contractAddress: "0x853d955aCEf822Db058eb8505911ED77F175b99e", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Gemini dollar", + symbol: "GUSD", + contractAddress: "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd", + decimal: 2, + enabled: false, + ), + Erc20Token( + name: "Compound Ether", + symbol: "cETH", + contractAddress: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Binance USD", + symbol: "BUSD", + contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "TrueUSD", + symbol: "TUSD", + contractAddress: "0x0000000000085d4780B73119b644AE5ecd22b376", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Cronos Coin", + symbol: "CRO", + contractAddress: "0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Pax Dollar", + symbol: "USDP", + contractAddress: "0x8E870D67F660D95d5be530380D0eC0bd388289E1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Fantom Token", + symbol: "FTM", + contractAddress: "0x4E15361FD6b4BB609Fa63C81A2be19d873717870", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BitTorrent", + symbol: "BTT", + contractAddress: "0xC669928185DbCE49d2230CC9B0979BE6DC797957", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Nexo", + symbol: "NEXO", + contractAddress: "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "dYdX", + symbol: "DYDX", + contractAddress: "0x92D6C1e31e14520e676a687F0a93788B716BEff5", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "PancakeSwap Token", + symbol: "Cake", + contractAddress: "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BAT", + symbol: "BAT", + contractAddress: "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "1INCH Token", + symbol: "1INCH", + contractAddress: "0x111111111117dC0aa78b770fA6A738034120C302", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Ethereum Name Service", + symbol: "ENS", + contractAddress: "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ZRX", + symbol: "ZRX", + contractAddress: "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Verse", + symbol: "VERSE", + contractAddress: "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18", + decimal: 18, + enabled: false, + ), + ]; + + List get initialErc20Tokens => _defaultTokens.map((token) { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + if (iconPath != null) { + return Erc20Token.copyWith(token, iconPath); + } + + return token; + }).toList(); +} diff --git a/cw_ethereum/lib/erc20_balance.dart b/cw_ethereum/lib/erc20_balance.dart new file mode 100644 index 000000000..7d11f8e45 --- /dev/null +++ b/cw_ethereum/lib/erc20_balance.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:cw_core/balance.dart'; + +class ERC20Balance extends Balance { + ERC20Balance(this.balance, {this.exponent = 18}) + : super(balance.toInt(), + balance.toInt()); + + final BigInt balance; + final int exponent; + + @override + String get formattedAdditionalBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + @override + String get formattedAvailableBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + String toJSON() => json.encode({ + 'balanceInWei': balance.toString(), + 'exponent': exponent, + }); + + static ERC20Balance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + try { + return ERC20Balance( + BigInt.parse(decoded['balanceInWei']), + exponent: decoded['exponent'], + ); + } catch (e) { + return ERC20Balance(BigInt.zero); + } + } +} diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart new file mode 100644 index 000000000..f00e2ef7b --- /dev/null +++ b/cw_ethereum/lib/ethereum_client.dart @@ -0,0 +1,230 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_ethereum/erc20_balance.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_ethereum/ethereum_transaction_model.dart'; +import 'package:cw_ethereum/pending_ethereum_transaction.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:web3dart/contracts/erc20.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +import 'package:cw_ethereum/.secrets.g.dart' as secrets; + +class EthereumClient { + final _httpClient = Client(); + Web3Client? _client; + + bool connect(Node node) { + try { + _client = Web3Client(node.uri.toString(), _httpClient); + + return true; + } catch (e) { + return false; + } + } + + void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async { + // _client?.pendingTransactions().listen((transactionHash) async { + // final transaction = await _client!.getTransactionByHash(transactionHash); + // + // if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) { + // onNewTransaction(); + // } + // }); + } + + Future getBalance(EthereumAddress address) async => + await _client!.getBalance(address); + + Future getGasUnitPrice() async { + final gasPrice = await _client!.getGasPrice(); + return gasPrice.getInWei.toInt(); + } + + Future getEstimatedGas() async { + final estimatedGas = await _client!.estimateGas(); + return estimatedGas.toInt(); + } + + Future signTransaction({ + required EthPrivateKey privateKey, + required String toAddress, + required String amount, + required int gas, + required EthereumTransactionPriority priority, + required CryptoCurrency currency, + required int exponent, + String? contractAddress, + }) async { + assert(currency == CryptoCurrency.eth || contractAddress != null); + + bool _isEthereum = currency == CryptoCurrency.eth; + + final price = await _client!.getGasPrice(); + + final Transaction transaction = Transaction( + from: privateKey.address, + to: EthereumAddress.fromHex(toAddress), + maxGas: gas, + gasPrice: price, + maxPriorityFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip), + value: _isEthereum ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), + ); + + final signedTransaction = await _client!.signTransaction(privateKey, transaction); + + final Function _sendTransaction; + + if (_isEthereum) { + _sendTransaction = () async => await sendTransaction(signedTransaction); + } else { + final erc20 = Erc20( + client: _client!, + address: EthereumAddress.fromHex(contractAddress!), + ); + + _sendTransaction = () async { + await erc20.transfer( + EthereumAddress.fromHex(toAddress), + BigInt.parse(amount), + credentials: privateKey, + ); + }; + } + + return PendingEthereumTransaction( + signedTransaction: signedTransaction, + amount: amount, + fee: BigInt.from(gas) * price.getInWei, + sendTransaction: _sendTransaction, + exponent: exponent, + ); + } + + Future sendTransaction(Uint8List signedTransaction) async => + await _client!.sendRawTransaction(signedTransaction); + + Future getTransactionDetails(String transactionHash) async { + // Wait for the transaction receipt to become available + TransactionReceipt? receipt; + while (receipt == null) { + receipt = await _client!.getTransactionReceipt(transactionHash); + await Future.delayed(Duration(seconds: 1)); + } + + // Print the receipt information + print('Transaction Hash: ${receipt.transactionHash}'); + print('Block Hash: ${receipt.blockHash}'); + print('Block Number: ${receipt.blockNumber}'); + print('Gas Used: ${receipt.gasUsed}'); + + /* + Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116] +I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200] +I/flutter ( 4474): Block Number: 17120242 +I/flutter ( 4474): Gas Used: 21000 + */ + + // Wait for the transaction receipt to become available + TransactionInformation? transactionInformation; + while (transactionInformation == null) { + print("********************************"); + transactionInformation = await _client!.getTransactionByHash(transactionHash); + await Future.delayed(Duration(seconds: 1)); + } + // Print the receipt information + print('Transaction Hash: ${transactionInformation.hash}'); + print('Block Hash: ${transactionInformation.blockHash}'); + print('Block Number: ${transactionInformation.blockNumber}'); + print('Gas Used: ${transactionInformation.gas}'); + + /* + Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74 +I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8 +I/flutter ( 4474): Block Number: 17120242 +I/flutter ( 4474): Gas Used: 53000 + */ + } + + Future fetchERC20Balances( + EthereumAddress userAddress, String contractAddress) async { + final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final balance = await erc20.balanceOf(userAddress); + + int exponent = (await erc20.decimals()).toInt(); + + return ERC20Balance(balance, exponent: exponent); + } + + Future getErc20Token(String contractAddress) async { + try { + final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final name = await erc20.name(); + final symbol = await erc20.symbol(); + final decimal = await erc20.decimals(); + + return Erc20Token( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal.toInt(), + ); + } catch (e) { + return null; + } + } + + void stop() { + _client?.dispose(); + } + + Future> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await _httpClient.get(Uri.https("api.etherscan.io", "/api", { + "module": "account", + "action": contractAddress != null ? "tokentx" : "txlist", + if (contractAddress != null) "contractaddress": contractAddress, + "address": address, + "apikey": secrets.etherScanApiKey, + })); + + final _jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && _jsonResponse['status'] != 0) { + return (_jsonResponse['result'] as List) + .map((e) => EthereumTransactionModel.fromJson(e as Map)) + .toList(); + } + + return []; + } catch (e) { + print(e); + return []; + } + } + +// Future _getDecimalPlacesForContract(DeployedContract contract) async { +// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json"); +// final contractAbi = ContractAbi.fromJson(abi, "ERC20"); +// +// final contract = DeployedContract( +// contractAbi, +// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!), +// ); +// final decimalsFunction = contract.function('decimals'); +// final decimals = await _client!.call( +// contract: contract, +// function: decimalsFunction, +// params: [], +// ); +// +// int exponent = int.parse(decimals.first.toString()); +// return exponent; +// } +} diff --git a/cw_ethereum/lib/ethereum_exceptions.dart b/cw_ethereum/lib/ethereum_exceptions.dart new file mode 100644 index 000000000..518f46275 --- /dev/null +++ b/cw_ethereum/lib/ethereum_exceptions.dart @@ -0,0 +1,11 @@ +import 'package:cw_core/crypto_currency.dart'; + +class EthereumTransactionCreationException implements Exception { + final String exceptionMessage; + + EthereumTransactionCreationException(CryptoCurrency currency) : + this.exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} diff --git a/cw_ethereum/lib/ethereum_formatter.dart b/cw_ethereum/lib/ethereum_formatter.dart new file mode 100644 index 000000000..468c536f8 --- /dev/null +++ b/cw_ethereum/lib/ethereum_formatter.dart @@ -0,0 +1,25 @@ +import 'package:intl/intl.dart'; + +const ethereumAmountLength = 12; +const ethereumAmountDivider = 1000000000000; +final ethereumAmountFormat = NumberFormat() + ..maximumFractionDigits = ethereumAmountLength + ..minimumFractionDigits = 1; + +class EthereumFormatter { + static int parseEthereumAmount(String amount) { + try { + return (double.parse(amount) * ethereumAmountDivider).round(); + } catch (_) { + return 0; + } + } + + static double parseEthereumAmountToDouble(int amount) { + try { + return amount / ethereumAmountDivider; + } catch (_) { + return 0; + } + } +} diff --git a/cw_ethereum/lib/ethereum_mnemonics.dart b/cw_ethereum/lib/ethereum_mnemonics.dart new file mode 100644 index 000000000..8af7b10f3 --- /dev/null +++ b/cw_ethereum/lib/ethereum_mnemonics.dart @@ -0,0 +1,2058 @@ +class EthereumMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} + +class EthereumMnemonics { + static const englishWordlist = [ + 'abandon', + 'ability', + 'able', + 'about', + 'above', + 'absent', + 'absorb', + 'abstract', + 'absurd', + 'abuse', + 'access', + 'accident', + 'account', + 'accuse', + 'achieve', + 'acid', + 'acoustic', + 'acquire', + 'across', + 'act', + 'action', + 'actor', + 'actress', + 'actual', + 'adapt', + 'add', + 'addict', + 'address', + 'adjust', + 'admit', + 'adult', + 'advance', + 'advice', + 'aerobic', + 'affair', + 'afford', + 'afraid', + 'again', + 'age', + 'agent', + 'agree', + 'ahead', + 'aim', + 'air', + 'airport', + 'aisle', + 'alarm', + 'album', + 'alcohol', + 'alert', + 'alien', + 'all', + 'alley', + 'allow', + 'almost', + 'alone', + 'alpha', + 'already', + 'also', + 'alter', + 'always', + 'amateur', + 'amazing', + 'among', + 'amount', + 'amused', + 'analyst', + 'anchor', + 'ancient', + 'anger', + 'angle', + 'angry', + 'animal', + 'ankle', + 'announce', + 'annual', + 'another', + 'answer', + 'antenna', + 'antique', + 'anxiety', + 'any', + 'apart', + 'apology', + 'appear', + 'apple', + 'approve', + 'april', + 'arch', + 'arctic', + 'area', + 'arena', + 'argue', + 'arm', + 'armed', + 'armor', + 'army', + 'around', + 'arrange', + 'arrest', + 'arrive', + 'arrow', + 'art', + 'artefact', + 'artist', + 'artwork', + 'ask', + 'aspect', + 'assault', + 'asset', + 'assist', + 'assume', + 'asthma', + 'athlete', + 'atom', + 'attack', + 'attend', + 'attitude', + 'attract', + 'auction', + 'audit', + 'august', + 'aunt', + 'author', + 'auto', + 'autumn', + 'average', + 'avocado', + 'avoid', + 'awake', + 'aware', + 'away', + 'awesome', + 'awful', + 'awkward', + 'axis', + 'baby', + 'bachelor', + 'bacon', + 'badge', + 'bag', + 'balance', + 'balcony', + 'ball', + 'bamboo', + 'banana', + 'banner', + 'bar', + 'barely', + 'bargain', + 'barrel', + 'base', + 'basic', + 'basket', + 'battle', + 'beach', + 'bean', + 'beauty', + 'because', + 'become', + 'beef', + 'before', + 'begin', + 'behave', + 'behind', + 'believe', + 'below', + 'belt', + 'bench', + 'benefit', + 'best', + 'betray', + 'better', + 'between', + 'beyond', + 'bicycle', + 'bid', + 'bike', + 'bind', + 'biology', + 'bird', + 'birth', + 'bitter', + 'black', + 'blade', + 'blame', + 'blanket', + 'blast', + 'bleak', + 'bless', + 'blind', + 'blood', + 'blossom', + 'blouse', + 'blue', + 'blur', + 'blush', + 'board', + 'boat', + 'body', + 'boil', + 'bomb', + 'bone', + 'bonus', + 'book', + 'boost', + 'border', + 'boring', + 'borrow', + 'boss', + 'bottom', + 'bounce', + 'box', + 'boy', + 'bracket', + 'brain', + 'brand', + 'brass', + 'brave', + 'bread', + 'breeze', + 'brick', + 'bridge', + 'brief', + 'bright', + 'bring', + 'brisk', + 'broccoli', + 'broken', + 'bronze', + 'broom', + 'brother', + 'brown', + 'brush', + 'bubble', + 'buddy', + 'budget', + 'buffalo', + 'build', + 'bulb', + 'bulk', + 'bullet', + 'bundle', + 'bunker', + 'burden', + 'burger', + 'burst', + 'bus', + 'business', + 'busy', + 'butter', + 'buyer', + 'buzz', + 'cabbage', + 'cabin', + 'cable', + 'cactus', + 'cage', + 'cake', + 'call', + 'calm', + 'camera', + 'camp', + 'can', + 'canal', + 'cancel', + 'candy', + 'cannon', + 'canoe', + 'canvas', + 'canyon', + 'capable', + 'capital', + 'captain', + 'car', + 'carbon', + 'card', + 'cargo', + 'carpet', + 'carry', + 'cart', + 'case', + 'cash', + 'casino', + 'castle', + 'casual', + 'cat', + 'catalog', + 'catch', + 'category', + 'cattle', + 'caught', + 'cause', + 'caution', + 'cave', + 'ceiling', + 'celery', + 'cement', + 'census', + 'century', + 'cereal', + 'certain', + 'chair', + 'chalk', + 'champion', + 'change', + 'chaos', + 'chapter', + 'charge', + 'chase', + 'chat', + 'cheap', + 'check', + 'cheese', + 'chef', + 'cherry', + 'chest', + 'chicken', + 'chief', + 'child', + 'chimney', + 'choice', + 'choose', + 'chronic', + 'chuckle', + 'chunk', + 'churn', + 'cigar', + 'cinnamon', + 'circle', + 'citizen', + 'city', + 'civil', + 'claim', + 'clap', + 'clarify', + 'claw', + 'clay', + 'clean', + 'clerk', + 'clever', + 'click', + 'client', + 'cliff', + 'climb', + 'clinic', + 'clip', + 'clock', + 'clog', + 'close', + 'cloth', + 'cloud', + 'clown', + 'club', + 'clump', + 'cluster', + 'clutch', + 'coach', + 'coast', + 'coconut', + 'code', + 'coffee', + 'coil', + 'coin', + 'collect', + 'color', + 'column', + 'combine', + 'come', + 'comfort', + 'comic', + 'common', + 'company', + 'concert', + 'conduct', + 'confirm', + 'congress', + 'connect', + 'consider', + 'control', + 'convince', + 'cook', + 'cool', + 'copper', + 'copy', + 'coral', + 'core', + 'corn', + 'correct', + 'cost', + 'cotton', + 'couch', + 'country', + 'couple', + 'course', + 'cousin', + 'cover', + 'coyote', + 'crack', + 'cradle', + 'craft', + 'cram', + 'crane', + 'crash', + 'crater', + 'crawl', + 'crazy', + 'cream', + 'credit', + 'creek', + 'crew', + 'cricket', + 'crime', + 'crisp', + 'critic', + 'crop', + 'cross', + 'crouch', + 'crowd', + 'crucial', + 'cruel', + 'cruise', + 'crumble', + 'crunch', + 'crush', + 'cry', + 'crystal', + 'cube', + 'culture', + 'cup', + 'cupboard', + 'curious', + 'current', + 'curtain', + 'curve', + 'cushion', + 'custom', + 'cute', + 'cycle', + 'dad', + 'damage', + 'damp', + 'dance', + 'danger', + 'daring', + 'dash', + 'daughter', + 'dawn', + 'day', + 'deal', + 'debate', + 'debris', + 'decade', + 'december', + 'decide', + 'decline', + 'decorate', + 'decrease', + 'deer', + 'defense', + 'define', + 'defy', + 'degree', + 'delay', + 'deliver', + 'demand', + 'demise', + 'denial', + 'dentist', + 'deny', + 'depart', + 'depend', + 'deposit', + 'depth', + 'deputy', + 'derive', + 'describe', + 'desert', + 'design', + 'desk', + 'despair', + 'destroy', + 'detail', + 'detect', + 'develop', + 'device', + 'devote', + 'diagram', + 'dial', + 'diamond', + 'diary', + 'dice', + 'diesel', + 'diet', + 'differ', + 'digital', + 'dignity', + 'dilemma', + 'dinner', + 'dinosaur', + 'direct', + 'dirt', + 'disagree', + 'discover', + 'disease', + 'dish', + 'dismiss', + 'disorder', + 'display', + 'distance', + 'divert', + 'divide', + 'divorce', + 'dizzy', + 'doctor', + 'document', + 'dog', + 'doll', + 'dolphin', + 'domain', + 'donate', + 'donkey', + 'donor', + 'door', + 'dose', + 'double', + 'dove', + 'draft', + 'dragon', + 'drama', + 'drastic', + 'draw', + 'dream', + 'dress', + 'drift', + 'drill', + 'drink', + 'drip', + 'drive', + 'drop', + 'drum', + 'dry', + 'duck', + 'dumb', + 'dune', + 'during', + 'dust', + 'dutch', + 'duty', + 'dwarf', + 'dynamic', + 'eager', + 'eagle', + 'early', + 'earn', + 'earth', + 'easily', + 'east', + 'easy', + 'echo', + 'ecology', + 'economy', + 'edge', + 'edit', + 'educate', + 'effort', + 'egg', + 'eight', + 'either', + 'elbow', + 'elder', + 'electric', + 'elegant', + 'element', + 'elephant', + 'elevator', + 'elite', + 'else', + 'embark', + 'embody', + 'embrace', + 'emerge', + 'emotion', + 'employ', + 'empower', + 'empty', + 'enable', + 'enact', + 'end', + 'endless', + 'endorse', + 'enemy', + 'energy', + 'enforce', + 'engage', + 'engine', + 'enhance', + 'enjoy', + 'enlist', + 'enough', + 'enrich', + 'enroll', + 'ensure', + 'enter', + 'entire', + 'entry', + 'envelope', + 'episode', + 'equal', + 'equip', + 'era', + 'erase', + 'erode', + 'erosion', + 'error', + 'erupt', + 'escape', + 'essay', + 'essence', + 'estate', + 'eternal', + 'ethics', + 'evidence', + 'evil', + 'evoke', + 'evolve', + 'exact', + 'example', + 'excess', + 'exchange', + 'excite', + 'exclude', + 'excuse', + 'execute', + 'exercise', + 'exhaust', + 'exhibit', + 'exile', + 'exist', + 'exit', + 'exotic', + 'expand', + 'expect', + 'expire', + 'explain', + 'expose', + 'express', + 'extend', + 'extra', + 'eye', + 'eyebrow', + 'fabric', + 'face', + 'faculty', + 'fade', + 'faint', + 'faith', + 'fall', + 'false', + 'fame', + 'family', + 'famous', + 'fan', + 'fancy', + 'fantasy', + 'farm', + 'fashion', + 'fat', + 'fatal', + 'father', + 'fatigue', + 'fault', + 'favorite', + 'feature', + 'february', + 'federal', + 'fee', + 'feed', + 'feel', + 'female', + 'fence', + 'festival', + 'fetch', + 'fever', + 'few', + 'fiber', + 'fiction', + 'field', + 'figure', + 'file', + 'film', + 'filter', + 'final', + 'find', + 'fine', + 'finger', + 'finish', + 'fire', + 'firm', + 'first', + 'fiscal', + 'fish', + 'fit', + 'fitness', + 'fix', + 'flag', + 'flame', + 'flash', + 'flat', + 'flavor', + 'flee', + 'flight', + 'flip', + 'float', + 'flock', + 'floor', + 'flower', + 'fluid', + 'flush', + 'fly', + 'foam', + 'focus', + 'fog', + 'foil', + 'fold', + 'follow', + 'food', + 'foot', + 'force', + 'forest', + 'forget', + 'fork', + 'fortune', + 'forum', + 'forward', + 'fossil', + 'foster', + 'found', + 'fox', + 'fragile', + 'frame', + 'frequent', + 'fresh', + 'friend', + 'fringe', + 'frog', + 'front', + 'frost', + 'frown', + 'frozen', + 'fruit', + 'fuel', + 'fun', + 'funny', + 'furnace', + 'fury', + 'future', + 'gadget', + 'gain', + 'galaxy', + 'gallery', + 'game', + 'gap', + 'garage', + 'garbage', + 'garden', + 'garlic', + 'garment', + 'gas', + 'gasp', + 'gate', + 'gather', + 'gauge', + 'gaze', + 'general', + 'genius', + 'genre', + 'gentle', + 'genuine', + 'gesture', + 'ghost', + 'giant', + 'gift', + 'giggle', + 'ginger', + 'giraffe', + 'girl', + 'give', + 'glad', + 'glance', + 'glare', + 'glass', + 'glide', + 'glimpse', + 'globe', + 'gloom', + 'glory', + 'glove', + 'glow', + 'glue', + 'goat', + 'goddess', + 'gold', + 'good', + 'goose', + 'gorilla', + 'gospel', + 'gossip', + 'govern', + 'gown', + 'grab', + 'grace', + 'grain', + 'grant', + 'grape', + 'grass', + 'gravity', + 'great', + 'green', + 'grid', + 'grief', + 'grit', + 'grocery', + 'group', + 'grow', + 'grunt', + 'guard', + 'guess', + 'guide', + 'guilt', + 'guitar', + 'gun', + 'gym', + 'habit', + 'hair', + 'half', + 'hammer', + 'hamster', + 'hand', + 'happy', + 'harbor', + 'hard', + 'harsh', + 'harvest', + 'hat', + 'have', + 'hawk', + 'hazard', + 'head', + 'health', + 'heart', + 'heavy', + 'hedgehog', + 'height', + 'hello', + 'helmet', + 'help', + 'hen', + 'hero', + 'hidden', + 'high', + 'hill', + 'hint', + 'hip', + 'hire', + 'history', + 'hobby', + 'hockey', + 'hold', + 'hole', + 'holiday', + 'hollow', + 'home', + 'honey', + 'hood', + 'hope', + 'horn', + 'horror', + 'horse', + 'hospital', + 'host', + 'hotel', + 'hour', + 'hover', + 'hub', + 'huge', + 'human', + 'humble', + 'humor', + 'hundred', + 'hungry', + 'hunt', + 'hurdle', + 'hurry', + 'hurt', + 'husband', + 'hybrid', + 'ice', + 'icon', + 'idea', + 'identify', + 'idle', + 'ignore', + 'ill', + 'illegal', + 'illness', + 'image', + 'imitate', + 'immense', + 'immune', + 'impact', + 'impose', + 'improve', + 'impulse', + 'inch', + 'include', + 'income', + 'increase', + 'index', + 'indicate', + 'indoor', + 'industry', + 'infant', + 'inflict', + 'inform', + 'inhale', + 'inherit', + 'initial', + 'inject', + 'injury', + 'inmate', + 'inner', + 'innocent', + 'input', + 'inquiry', + 'insane', + 'insect', + 'inside', + 'inspire', + 'install', + 'intact', + 'interest', + 'into', + 'invest', + 'invite', + 'involve', + 'iron', + 'island', + 'isolate', + 'issue', + 'item', + 'ivory', + 'jacket', + 'jaguar', + 'jar', + 'jazz', + 'jealous', + 'jeans', + 'jelly', + 'jewel', + 'job', + 'join', + 'joke', + 'journey', + 'joy', + 'judge', + 'juice', + 'jump', + 'jungle', + 'junior', + 'junk', + 'just', + 'kangaroo', + 'keen', + 'keep', + 'ketchup', + 'key', + 'kick', + 'kid', + 'kidney', + 'kind', + 'kingdom', + 'kiss', + 'kit', + 'kitchen', + 'kite', + 'kitten', + 'kiwi', + 'knee', + 'knife', + 'knock', + 'know', + 'lab', + 'label', + 'labor', + 'ladder', + 'lady', + 'lake', + 'lamp', + 'language', + 'laptop', + 'large', + 'later', + 'latin', + 'laugh', + 'laundry', + 'lava', + 'law', + 'lawn', + 'lawsuit', + 'layer', + 'lazy', + 'leader', + 'leaf', + 'learn', + 'leave', + 'lecture', + 'left', + 'leg', + 'legal', + 'legend', + 'leisure', + 'lemon', + 'lend', + 'length', + 'lens', + 'leopard', + 'lesson', + 'letter', + 'level', + 'liar', + 'liberty', + 'library', + 'license', + 'life', + 'lift', + 'light', + 'like', + 'limb', + 'limit', + 'link', + 'lion', + 'liquid', + 'list', + 'little', + 'live', + 'lizard', + 'load', + 'loan', + 'lobster', + 'local', + 'lock', + 'logic', + 'lonely', + 'long', + 'loop', + 'lottery', + 'loud', + 'lounge', + 'love', + 'loyal', + 'lucky', + 'luggage', + 'lumber', + 'lunar', + 'lunch', + 'luxury', + 'lyrics', + 'machine', + 'mad', + 'magic', + 'magnet', + 'maid', + 'mail', + 'main', + 'major', + 'make', + 'mammal', + 'man', + 'manage', + 'mandate', + 'mango', + 'mansion', + 'manual', + 'maple', + 'marble', + 'march', + 'margin', + 'marine', + 'market', + 'marriage', + 'mask', + 'mass', + 'master', + 'match', + 'material', + 'math', + 'matrix', + 'matter', + 'maximum', + 'maze', + 'meadow', + 'mean', + 'measure', + 'meat', + 'mechanic', + 'medal', + 'media', + 'melody', + 'melt', + 'member', + 'memory', + 'mention', + 'menu', + 'mercy', + 'merge', + 'merit', + 'merry', + 'mesh', + 'message', + 'metal', + 'method', + 'middle', + 'midnight', + 'milk', + 'million', + 'mimic', + 'mind', + 'minimum', + 'minor', + 'minute', + 'miracle', + 'mirror', + 'misery', + 'miss', + 'mistake', + 'mix', + 'mixed', + 'mixture', + 'mobile', + 'model', + 'modify', + 'mom', + 'moment', + 'monitor', + 'monkey', + 'monster', + 'month', + 'moon', + 'moral', + 'more', + 'morning', + 'mosquito', + 'mother', + 'motion', + 'motor', + 'mountain', + 'mouse', + 'move', + 'movie', + 'much', + 'muffin', + 'mule', + 'multiply', + 'muscle', + 'museum', + 'mushroom', + 'music', + 'must', + 'mutual', + 'myself', + 'mystery', + 'myth', + 'naive', + 'name', + 'napkin', + 'narrow', + 'nasty', + 'nation', + 'nature', + 'near', + 'neck', + 'need', + 'negative', + 'neglect', + 'neither', + 'nephew', + 'nerve', + 'nest', + 'net', + 'network', + 'neutral', + 'never', + 'news', + 'next', + 'nice', + 'night', + 'noble', + 'noise', + 'nominee', + 'noodle', + 'normal', + 'north', + 'nose', + 'notable', + 'note', + 'nothing', + 'notice', + 'novel', + 'now', + 'nuclear', + 'number', + 'nurse', + 'nut', + 'oak', + 'obey', + 'object', + 'oblige', + 'obscure', + 'observe', + 'obtain', + 'obvious', + 'occur', + 'ocean', + 'october', + 'odor', + 'off', + 'offer', + 'office', + 'often', + 'oil', + 'okay', + 'old', + 'olive', + 'olympic', + 'omit', + 'once', + 'one', + 'onion', + 'online', + 'only', + 'open', + 'opera', + 'opinion', + 'oppose', + 'option', + 'orange', + 'orbit', + 'orchard', + 'order', + 'ordinary', + 'organ', + 'orient', + 'original', + 'orphan', + 'ostrich', + 'other', + 'outdoor', + 'outer', + 'output', + 'outside', + 'oval', + 'oven', + 'over', + 'own', + 'owner', + 'oxygen', + 'oyster', + 'ozone', + 'pact', + 'paddle', + 'page', + 'pair', + 'palace', + 'palm', + 'panda', + 'panel', + 'panic', + 'panther', + 'paper', + 'parade', + 'parent', + 'park', + 'parrot', + 'party', + 'pass', + 'patch', + 'path', + 'patient', + 'patrol', + 'pattern', + 'pause', + 'pave', + 'payment', + 'peace', + 'peanut', + 'pear', + 'peasant', + 'pelican', + 'pen', + 'penalty', + 'pencil', + 'people', + 'pepper', + 'perfect', + 'permit', + 'person', + 'pet', + 'phone', + 'photo', + 'phrase', + 'physical', + 'piano', + 'picnic', + 'picture', + 'piece', + 'pig', + 'pigeon', + 'pill', + 'pilot', + 'pink', + 'pioneer', + 'pipe', + 'pistol', + 'pitch', + 'pizza', + 'place', + 'planet', + 'plastic', + 'plate', + 'play', + 'please', + 'pledge', + 'pluck', + 'plug', + 'plunge', + 'poem', + 'poet', + 'point', + 'polar', + 'pole', + 'police', + 'pond', + 'pony', + 'pool', + 'popular', + 'portion', + 'position', + 'possible', + 'post', + 'potato', + 'pottery', + 'poverty', + 'powder', + 'power', + 'practice', + 'praise', + 'predict', + 'prefer', + 'prepare', + 'present', + 'pretty', + 'prevent', + 'price', + 'pride', + 'primary', + 'print', + 'priority', + 'prison', + 'private', + 'prize', + 'problem', + 'process', + 'produce', + 'profit', + 'program', + 'project', + 'promote', + 'proof', + 'property', + 'prosper', + 'protect', + 'proud', + 'provide', + 'public', + 'pudding', + 'pull', + 'pulp', + 'pulse', + 'pumpkin', + 'punch', + 'pupil', + 'puppy', + 'purchase', + 'purity', + 'purpose', + 'purse', + 'push', + 'put', + 'puzzle', + 'pyramid', + 'quality', + 'quantum', + 'quarter', + 'question', + 'quick', + 'quit', + 'quiz', + 'quote', + 'rabbit', + 'raccoon', + 'race', + 'rack', + 'radar', + 'radio', + 'rail', + 'rain', + 'raise', + 'rally', + 'ramp', + 'ranch', + 'random', + 'range', + 'rapid', + 'rare', + 'rate', + 'rather', + 'raven', + 'raw', + 'razor', + 'ready', + 'real', + 'reason', + 'rebel', + 'rebuild', + 'recall', + 'receive', + 'recipe', + 'record', + 'recycle', + 'reduce', + 'reflect', + 'reform', + 'refuse', + 'region', + 'regret', + 'regular', + 'reject', + 'relax', + 'release', + 'relief', + 'rely', + 'remain', + 'remember', + 'remind', + 'remove', + 'render', + 'renew', + 'rent', + 'reopen', + 'repair', + 'repeat', + 'replace', + 'report', + 'require', + 'rescue', + 'resemble', + 'resist', + 'resource', + 'response', + 'result', + 'retire', + 'retreat', + 'return', + 'reunion', + 'reveal', + 'review', + 'reward', + 'rhythm', + 'rib', + 'ribbon', + 'rice', + 'rich', + 'ride', + 'ridge', + 'rifle', + 'right', + 'rigid', + 'ring', + 'riot', + 'ripple', + 'risk', + 'ritual', + 'rival', + 'river', + 'road', + 'roast', + 'robot', + 'robust', + 'rocket', + 'romance', + 'roof', + 'rookie', + 'room', + 'rose', + 'rotate', + 'rough', + 'round', + 'route', + 'royal', + 'rubber', + 'rude', + 'rug', + 'rule', + 'run', + 'runway', + 'rural', + 'sad', + 'saddle', + 'sadness', + 'safe', + 'sail', + 'salad', + 'salmon', + 'salon', + 'salt', + 'salute', + 'same', + 'sample', + 'sand', + 'satisfy', + 'satoshi', + 'sauce', + 'sausage', + 'save', + 'say', + 'scale', + 'scan', + 'scare', + 'scatter', + 'scene', + 'scheme', + 'school', + 'science', + 'scissors', + 'scorpion', + 'scout', + 'scrap', + 'screen', + 'script', + 'scrub', + 'sea', + 'search', + 'season', + 'seat', + 'second', + 'secret', + 'section', + 'security', + 'seed', + 'seek', + 'segment', + 'select', + 'sell', + 'seminar', + 'senior', + 'sense', + 'sentence', + 'series', + 'service', + 'session', + 'settle', + 'setup', + 'seven', + 'shadow', + 'shaft', + 'shallow', + 'share', + 'shed', + 'shell', + 'sheriff', + 'shield', + 'shift', + 'shine', + 'ship', + 'shiver', + 'shock', + 'shoe', + 'shoot', + 'shop', + 'short', + 'shoulder', + 'shove', + 'shrimp', + 'shrug', + 'shuffle', + 'shy', + 'sibling', + 'sick', + 'side', + 'siege', + 'sight', + 'sign', + 'silent', + 'silk', + 'silly', + 'silver', + 'similar', + 'simple', + 'since', + 'sing', + 'siren', + 'sister', + 'situate', + 'six', + 'size', + 'skate', + 'sketch', + 'ski', + 'skill', + 'skin', + 'skirt', + 'skull', + 'slab', + 'slam', + 'sleep', + 'slender', + 'slice', + 'slide', + 'slight', + 'slim', + 'slogan', + 'slot', + 'slow', + 'slush', + 'small', + 'smart', + 'smile', + 'smoke', + 'smooth', + 'snack', + 'snake', + 'snap', + 'sniff', + 'snow', + 'soap', + 'soccer', + 'social', + 'sock', + 'soda', + 'soft', + 'solar', + 'soldier', + 'solid', + 'solution', + 'solve', + 'someone', + 'song', + 'soon', + 'sorry', + 'sort', + 'soul', + 'sound', + 'soup', + 'source', + 'south', + 'space', + 'spare', + 'spatial', + 'spawn', + 'speak', + 'special', + 'speed', + 'spell', + 'spend', + 'sphere', + 'spice', + 'spider', + 'spike', + 'spin', + 'spirit', + 'split', + 'spoil', + 'sponsor', + 'spoon', + 'sport', + 'spot', + 'spray', + 'spread', + 'spring', + 'spy', + 'square', + 'squeeze', + 'squirrel', + 'stable', + 'stadium', + 'staff', + 'stage', + 'stairs', + 'stamp', + 'stand', + 'start', + 'state', + 'stay', + 'steak', + 'steel', + 'stem', + 'step', + 'stereo', + 'stick', + 'still', + 'sting', + 'stock', + 'stomach', + 'stone', + 'stool', + 'story', + 'stove', + 'strategy', + 'street', + 'strike', + 'strong', + 'struggle', + 'student', + 'stuff', + 'stumble', + 'style', + 'subject', + 'submit', + 'subway', + 'success', + 'such', + 'sudden', + 'suffer', + 'sugar', + 'suggest', + 'suit', + 'summer', + 'sun', + 'sunny', + 'sunset', + 'super', + 'supply', + 'supreme', + 'sure', + 'surface', + 'surge', + 'surprise', + 'surround', + 'survey', + 'suspect', + 'sustain', + 'swallow', + 'swamp', + 'swap', + 'swarm', + 'swear', + 'sweet', + 'swift', + 'swim', + 'swing', + 'switch', + 'sword', + 'symbol', + 'symptom', + 'syrup', + 'system', + 'table', + 'tackle', + 'tag', + 'tail', + 'talent', + 'talk', + 'tank', + 'tape', + 'target', + 'task', + 'taste', + 'tattoo', + 'taxi', + 'teach', + 'team', + 'tell', + 'ten', + 'tenant', + 'tennis', + 'tent', + 'term', + 'test', + 'text', + 'thank', + 'that', + 'theme', + 'then', + 'theory', + 'there', + 'they', + 'thing', + 'this', + 'thought', + 'three', + 'thrive', + 'throw', + 'thumb', + 'thunder', + 'ticket', + 'tide', + 'tiger', + 'tilt', + 'timber', + 'time', + 'tiny', + 'tip', + 'tired', + 'tissue', + 'title', + 'toast', + 'tobacco', + 'today', + 'toddler', + 'toe', + 'together', + 'toilet', + 'token', + 'tomato', + 'tomorrow', + 'tone', + 'tongue', + 'tonight', + 'tool', + 'tooth', + 'top', + 'topic', + 'topple', + 'torch', + 'tornado', + 'tortoise', + 'toss', + 'total', + 'tourist', + 'toward', + 'tower', + 'town', + 'toy', + 'track', + 'trade', + 'traffic', + 'tragic', + 'train', + 'transfer', + 'trap', + 'trash', + 'travel', + 'tray', + 'treat', + 'tree', + 'trend', + 'trial', + 'tribe', + 'trick', + 'trigger', + 'trim', + 'trip', + 'trophy', + 'trouble', + 'truck', + 'true', + 'truly', + 'trumpet', + 'trust', + 'truth', + 'try', + 'tube', + 'tuition', + 'tumble', + 'tuna', + 'tunnel', + 'turkey', + 'turn', + 'turtle', + 'twelve', + 'twenty', + 'twice', + 'twin', + 'twist', + 'two', + 'type', + 'typical', + 'ugly', + 'umbrella', + 'unable', + 'unaware', + 'uncle', + 'uncover', + 'under', + 'undo', + 'unfair', + 'unfold', + 'unhappy', + 'uniform', + 'unique', + 'unit', + 'universe', + 'unknown', + 'unlock', + 'until', + 'unusual', + 'unveil', + 'update', + 'upgrade', + 'uphold', + 'upon', + 'upper', + 'upset', + 'urban', + 'urge', + 'usage', + 'use', + 'used', + 'useful', + 'useless', + 'usual', + 'utility', + 'vacant', + 'vacuum', + 'vague', + 'valid', + 'valley', + 'valve', + 'van', + 'vanish', + 'vapor', + 'various', + 'vast', + 'vault', + 'vehicle', + 'velvet', + 'vendor', + 'venture', + 'venue', + 'verb', + 'verify', + 'version', + 'very', + 'vessel', + 'veteran', + 'viable', + 'vibrant', + 'vicious', + 'victory', + 'video', + 'view', + 'village', + 'vintage', + 'violin', + 'virtual', + 'virus', + 'visa', + 'visit', + 'visual', + 'vital', + 'vivid', + 'vocal', + 'voice', + 'void', + 'volcano', + 'volume', + 'vote', + 'voyage', + 'wage', + 'wagon', + 'wait', + 'walk', + 'wall', + 'walnut', + 'want', + 'warfare', + 'warm', + 'warrior', + 'wash', + 'wasp', + 'waste', + 'water', + 'wave', + 'way', + 'wealth', + 'weapon', + 'wear', + 'weasel', + 'weather', + 'web', + 'wedding', + 'weekend', + 'weird', + 'welcome', + 'west', + 'wet', + 'whale', + 'what', + 'wheat', + 'wheel', + 'when', + 'where', + 'whip', + 'whisper', + 'wide', + 'width', + 'wife', + 'wild', + 'will', + 'win', + 'window', + 'wine', + 'wing', + 'wink', + 'winner', + 'winter', + 'wire', + 'wisdom', + 'wise', + 'wish', + 'witness', + 'wolf', + 'woman', + 'wonder', + 'wood', + 'wool', + 'word', + 'work', + 'world', + 'worry', + 'worth', + 'wrap', + 'wreck', + 'wrestle', + 'wrist', + 'write', + 'wrong', + 'yard', + 'year', + 'yellow', + 'you', + 'young', + 'youth', + 'zebra', + 'zero', + 'zone', + 'zoo' + ]; +} diff --git a/cw_ethereum/lib/ethereum_transaction_credentials.dart b/cw_ethereum/lib/ethereum_transaction_credentials.dart new file mode 100644 index 000000000..b015b7141 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_credentials.dart @@ -0,0 +1,17 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; + +class EthereumTransactionCredentials { + EthereumTransactionCredentials( + this.outputs, { + required this.priority, + required this.currency, + this.feeRate, + }); + + final List outputs; + final EthereumTransactionPriority? priority; + final int? feeRate; + final CryptoCurrency currency; +} diff --git a/cw_ethereum/lib/ethereum_transaction_history.dart b/cw_ethereum/lib/ethereum_transaction_history.dart new file mode 100644 index 000000000..4511f4436 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_history.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_ethereum/file.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; + +part 'ethereum_transaction_history.g.dart'; + +const transactionsHistoryFileName = 'transactions.json'; + +class EthereumTransactionHistory = EthereumTransactionHistoryBase with _$EthereumTransactionHistory; + +abstract class EthereumTransactionHistoryBase + extends TransactionHistoryBase with Store { + EthereumTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + String _password; + + Future init() async => await _load(); + + @override + Future save() async { + try { + 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); + } catch (e, s) { + print('Error while save ethereum transaction history: ${e.toString()}'); + print(s); + } + } + + @override + void addOne(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + 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); + if (content.isEmpty) { + return {}; + } + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = EthereumTransactionInfo.fromJson(val); + _update(tx); + } + }); + } catch (e) { + print(e); + } + } + + void _update(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart new file mode 100644 index 000000000..efdc61407 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_info.dart @@ -0,0 +1,74 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; + +class EthereumTransactionInfo extends TransactionInfo { + EthereumTransactionInfo({ + required this.id, + required this.height, + required this.ethAmount, + required this.ethFee, + this.tokenSymbol = "ETH", + this.exponent = 18, + required this.direction, + required this.isPending, + required this.date, + required this.confirmations, + }) : this.amount = ethAmount.toInt(), + this.fee = ethFee.toInt(); + + final String id; + final int height; + final int amount; + final BigInt ethAmount; + final int exponent; + final TransactionDirection direction; + final DateTime date; + final bool isPending; + final int fee; + final BigInt ethFee; + final int confirmations; + final String tokenSymbol; + String? _fiatAmount; + + @override + String amountFormatted() => + '${formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString())} $tokenSymbol'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} ETH'; + + factory EthereumTransactionInfo.fromJson(Map data) { + return EthereumTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + ethAmount: BigInt.parse(data['amount']), + exponent: data['exponent'] as int, + ethFee: BigInt.parse(data['fee']), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + ); + } + + Map toJson() => { + 'id': id, + 'height': height, + 'amount': ethAmount.toString(), + 'exponent': exponent, + 'fee': ethFee.toString(), + 'direction': direction.index, + 'date': date.millisecondsSinceEpoch, + 'isPending': isPending, + 'confirmations': confirmations, + 'tokenSymbol': tokenSymbol, + }; +} diff --git a/cw_ethereum/lib/ethereum_transaction_model.dart b/cw_ethereum/lib/ethereum_transaction_model.dart new file mode 100644 index 000000000..c1260795a --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_model.dart @@ -0,0 +1,47 @@ +class EthereumTransactionModel { + final DateTime date; + final String hash; + final String from; + final String to; + final BigInt amount; + final int gasUsed; + final BigInt gasPrice; + final String contractAddress; + final int confirmations; + final int blockNumber; + final String? tokenSymbol; + final int? tokenDecimal; + final bool isError; + + EthereumTransactionModel({ + required this.date, + required this.hash, + required this.from, + required this.to, + required this.amount, + required this.gasUsed, + required this.gasPrice, + required this.contractAddress, + required this.confirmations, + required this.blockNumber, + required this.tokenSymbol, + required this.tokenDecimal, + required this.isError, + }); + + factory EthereumTransactionModel.fromJson(Map json) => EthereumTransactionModel( + date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000), + hash: json["hash"], + from: json["from"], + to: json["to"], + amount: BigInt.parse(json["value"]), + gasUsed: int.parse(json["gasUsed"]), + gasPrice: BigInt.parse(json["gasPrice"]), + contractAddress: json["contractAddress"], + confirmations: int.parse(json["confirmations"]), + blockNumber: int.parse(json["blockNumber"]), + tokenSymbol: json["tokenSymbol"] ?? "ETH", + tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), + isError: json["isError"] == "1", + ); +} diff --git a/cw_ethereum/lib/ethereum_transaction_priority.dart b/cw_ethereum/lib/ethereum_transaction_priority.dart new file mode 100644 index 000000000..ff5668397 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_priority.dart @@ -0,0 +1,52 @@ +import 'package:cw_core/transaction_priority.dart'; + +class EthereumTransactionPriority extends TransactionPriority { + final int tip; + + const EthereumTransactionPriority({required String title, required int raw, required this.tip}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const EthereumTransactionPriority slow = + EthereumTransactionPriority(title: 'slow', raw: 0, tip: 1); + static const EthereumTransactionPriority medium = + EthereumTransactionPriority(title: 'Medium', raw: 1, tip: 2); + static const EthereumTransactionPriority fast = + EthereumTransactionPriority(title: 'Fast', raw: 2, tip: 4); + + static EthereumTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for EthereumTransactionPriority deserialize'); + } + } + + String get units => 'gas'; + + @override + String toString() { + var label = ''; + + switch (this) { + case EthereumTransactionPriority.slow: + label = 'Slow'; + break; + case EthereumTransactionPriority.medium: + label = 'Medium'; + break; + case EthereumTransactionPriority.fast: + label = 'Fast'; + break; + default: + break; + } + + return label; + } +} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart new file mode 100644 index 000000000..46cb5c39f --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -0,0 +1,473 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/crypto_currency.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/wallet_addresses.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_ethereum/default_erc20_tokens.dart'; +import 'package:cw_ethereum/erc20_balance.dart'; +import 'package:cw_ethereum/ethereum_client.dart'; +import 'package:cw_ethereum/ethereum_exceptions.dart'; +import 'package:cw_ethereum/ethereum_formatter.dart'; +import 'package:cw_ethereum/ethereum_transaction_credentials.dart'; +import 'package:cw_ethereum/ethereum_transaction_history.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; +import 'package:cw_ethereum/ethereum_transaction_model.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; +import 'package:cw_ethereum/file.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:hive/hive.dart'; +import 'package:hex/hex.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bip32/bip32.dart' as bip32; + +part 'ethereum_wallet.g.dart'; + +class EthereumWallet = EthereumWalletBase with _$EthereumWallet; + +abstract class EthereumWalletBase + extends WalletBase + with Store { + EthereumWalletBase({ + required WalletInfo walletInfo, + required String mnemonic, + required String password, + ERC20Balance? initialBalance, + }) : syncStatus = NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _isTransactionUpdating = false, + _client = EthereumClient(), + walletAddresses = EthereumWalletAddresses(walletInfo), + balance = ObservableMap.of( + {CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = EthereumTransactionHistory(walletInfo: walletInfo, password: password); + + if (!Hive.isAdapterRegistered(Erc20Token.typeId)) { + Hive.registerAdapter(Erc20TokenAdapter()); + } + + _sharedPrefs.complete(SharedPreferences.getInstance()); + } + + final String _mnemonic; + final String _password; + + late final Box erc20TokensBox; + + late final EthPrivateKey _privateKey; + + late EthereumClient _client; + + int? _gasPrice; + int? _estimatedGas; + bool _isTransactionUpdating; + + // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter + Timer? _transactionsUpdateTimer; + + @override + WalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + Completer _sharedPrefs = Completer(); + + Future init() async { + erc20TokensBox = await Hive.openBox(Erc20Token.boxName); + await walletAddresses.init(); + await transactionHistory.init(); + _privateKey = await getPrivateKey(_mnemonic, _password); + walletAddresses.address = _privateKey.address.toString(); + await save(); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + try { + if (priority is EthereumTransactionPriority) { + final priorityFee = + EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip).getInWei.toInt(); + return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); + } + + return 0; + } catch (e) { + return 0; + } + } + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _client.stop(); + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("Ethereum Node connection failed"); + } + + _client.setListeners(_privateKey.address, _onNewTransaction); + + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + final _credentials = credentials as EthereumTransactionCredentials; + final outputs = _credentials.outputs; + final hasMultiDestination = outputs.length > 1; + final _erc20Balance = balance[_credentials.currency]!; + BigInt totalAmount = BigInt.zero; + int exponent = + _credentials.currency is Erc20Token ? (_credentials.currency as Erc20Token).decimal : 18; + num amountToEthereumMultiplier = pow(10, exponent); + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw EthereumTransactionCreationException(_credentials.currency); + } + + final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble( + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); + totalAmount = BigInt.from(totalOriginalAmount * amountToEthereumMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EthereumTransactionCreationException(_credentials.currency); + } + } else { + final output = outputs.first; + final BigInt allAmount = + _erc20Balance.balance - BigInt.from(calculateEstimatedFee(_credentials.priority!, null)); + final totalOriginalAmount = + EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0); + totalAmount = output.sendAll + ? allAmount + : BigInt.from(totalOriginalAmount * amountToEthereumMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EthereumTransactionCreationException(_credentials.currency); + } + } + + final pendingEthereumTransaction = await _client.signTransaction( + privateKey: _privateKey, + toAddress: _credentials.outputs.first.address, + amount: totalAmount.toString(), + gas: _estimatedGas!, + priority: _credentials.priority!, + currency: _credentials.currency, + exponent: exponent, + contractAddress: _credentials.currency is Erc20Token + ? (_credentials.currency as Erc20Token).contractAddress + : null, + ); + + return pendingEthereumTransaction; + } + + Future _updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + bool isEtherscanEnabled = (await _sharedPrefs.future).getBool("use_etherscan") ?? true; + if (!isEtherscanEnabled) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (_) { + _isTransactionUpdating = false; + } + } + + @override + Future> fetchTransactions() async { + final address = _privateKey.address.hex; + final transactions = await _client.fetchTransactions(address); + + final List>> erc20TokensTransactions = []; + + for (var token in balance.keys) { + if (token is Erc20Token) { + erc20TokensTransactions.add(_client.fetchTransactions( + address, + contractAddress: token.contractAddress, + )); + } + } + + final tokensTransaction = await Future.wait(erc20TokensTransactions); + transactions.addAll(tokensTransaction.expand((element) => element)); + + final Map result = {}; + + for (var transactionModel in transactions) { + if (transactionModel.isError) { + continue; + } + + result[transactionModel.hash] = EthereumTransactionInfo( + id: transactionModel.hash, + height: transactionModel.blockNumber, + ethAmount: transactionModel.amount, + direction: transactionModel.from == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + isPending: false, + date: transactionModel.date, + confirmations: transactionModel.confirmations, + ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, + exponent: transactionModel.tokenDecimal ?? 18, + tokenSymbol: transactionModel.tokenSymbol ?? "ETH", + ); + } + + return result; + } + + @override + Object get keys => throw UnimplementedError("keys"); + + @override + Future rescan({required int height}) { + throw UnimplementedError("rescan"); + } + + @override + Future save() async { + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @override + String get seed => _mnemonic; + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + await _updateBalance(); + await _updateTransactions(); + _gasPrice = await _client.getGasUnitPrice(); + _estimatedGas = await _client.getEstimatedGas(); + + Timer.periodic( + const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice()); + Timer.periodic(const Duration(seconds: 10), + (timer) async => _estimatedGas = await _client.getEstimatedGas()); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'balance': balance[currency]!.toJSON(), + }); + + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + }) async { + 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 balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero); + + return EthereumWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + initialBalance: balance, + ); + } + + Future _updateBalance() async { + balance[currency] = await _fetchEthBalance(); + + await _fetchErc20Balances(); + await save(); + } + + Future _fetchEthBalance() async { + final balance = await _client.getBalance(_privateKey.address); + return ERC20Balance(balance.getInWei); + } + + Future _fetchErc20Balances() async { + for (var token in erc20TokensBox.values) { + try { + if (token.enabled) { + balance[token] = await _client.fetchERC20Balances( + _privateKey.address, + token.contractAddress, + ); + } else { + balance.remove(token); + } + } catch (_) {} + } + } + + Future getPrivateKey(String mnemonic, String password) async { + final seed = bip39.mnemonicToSeed(mnemonic); + + final root = bip32.BIP32.fromSeed(seed); + + const _hdPathEthereum = "m/44'/60'/0'/0"; + const index = 0; + final addressAtIndex = root.derivePath("$_hdPathEthereum/$index"); + + return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List)); + } + + Future? updateBalance() async => await _updateBalance(); + + List get erc20Currencies => erc20TokensBox.values.toList(); + + Future addErc20Token(Erc20Token token) async { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + final _token = Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + iconPath: iconPath, + ); + + await erc20TokensBox.put(_token.contractAddress, _token); + + if (_token.enabled) { + balance[_token] = await _client.fetchERC20Balances( + _privateKey.address, + _token.contractAddress, + ); + } else { + balance.remove(_token); + } + } + + Future deleteErc20Token(Erc20Token token) async { + await token.delete(); + + balance.remove(token); + _updateBalance(); + } + + Future getErc20Token(String contractAddress) async => + await _client.getErc20Token(contractAddress); + + void _onNewTransaction() { + _updateBalance(); + _updateTransactions(); + } + + void addInitialTokens() { + final initialErc20Tokens = DefaultErc20Tokens().initialErc20Tokens; + + initialErc20Tokens.forEach((token) => erc20TokensBox.put(token.contractAddress, token)); + } + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + + void _setTransactionUpdateTimer() { + if (_transactionsUpdateTimer?.isActive ?? false) { + _transactionsUpdateTimer!.cancel(); + } + + _transactionsUpdateTimer = Timer.periodic(Duration(seconds: 10), (_) { + _updateTransactions(); + _updateBalance(); + }); + } + + void updateEtherscanUsageState(bool isEnabled) { + if (isEnabled) { + _updateTransactions(); + _setTransactionUpdateTimer(); + } else { + _transactionsUpdateTimer?.cancel(); + } + } +} diff --git a/cw_ethereum/lib/ethereum_wallet_addresses.dart b/cw_ethereum/lib/ethereum_wallet_addresses.dart new file mode 100644 index 000000000..4a3492e6f --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_addresses.dart @@ -0,0 +1,33 @@ +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'ethereum_wallet_addresses.g.dart'; + +class EthereumWalletAddresses = EthereumWalletAddressesBase with _$EthereumWalletAddresses; + +abstract class EthereumWalletAddressesBase extends WalletAddresses with Store { + EthereumWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + String address; + + @override + Future init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart new file mode 100644 index 000000000..12d0d53e2 --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart @@ -0,0 +1,23 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class EthereumNewWalletCredentials extends WalletCredentials { + EthereumNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class EthereumRestoreWalletFromSeedCredentials extends WalletCredentials { + EthereumRestoreWalletFromSeedCredentials( + {required String name, required String password, required this.mnemonic, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class EthereumRestoreWalletFromWIFCredentials extends WalletCredentials { + EthereumRestoreWalletFromWIFCredentials( + {required String name, required String password, required this.wif, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String wif; +} diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart new file mode 100644 index 000000000..318f287fc --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -0,0 +1,108 @@ +import 'dart:io'; + +import 'package:cw_core/pathForWallet.dart'; +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:cw_ethereum/ethereum_mnemonics.dart'; +import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:collection/collection.dart'; + +class EthereumWalletService extends WalletService { + EthereumWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + Future create(EthereumNewWalletCredentials credentials) async { + final mnemonic = bip39.generateMnemonic(); + final wallet = EthereumWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + WalletType getType() => WalletType.ethereum; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + final wallet = await EthereumWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + + return wallet; + } + + @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()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future restoreFromKeys(credentials) { + throw UnimplementedError(); + } + + @override + Future restoreFromSeed( + EthereumRestoreWalletFromSeedCredentials credentials) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw EthereumMnemonicIsIncorrectException(); + } + + final wallet = EthereumWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await EthereumWalletBase.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } +} diff --git a/cw_ethereum/lib/file.dart b/cw_ethereum/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_ethereum/lib/file.dart @@ -0,0 +1,39 @@ +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_ethereum/lib/pending_ethereum_transaction.dart b/cw_ethereum/lib/pending_ethereum_transaction.dart new file mode 100644 index 000000000..23dfa3b87 --- /dev/null +++ b/cw_ethereum/lib/pending_ethereum_transaction.dart @@ -0,0 +1,36 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:cw_core/pending_transaction.dart'; +import 'package:web3dart/crypto.dart'; + +class PendingEthereumTransaction with PendingTransaction { + final Function sendTransaction; + final Uint8List signedTransaction; + final BigInt fee; + final String amount; + final int exponent; + + PendingEthereumTransaction({ + required this.sendTransaction, + required this.signedTransaction, + required this.fee, + required this.amount, + required this.exponent, + }); + + @override + String get amountFormatted => (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString(); + + @override + Future commit() async => await sendTransaction(); + + @override + String get feeFormatted => (fee / BigInt.from(pow(10, 18))).toString(); + + @override + String get hex => bytesToHex(signedTransaction, include0x: true); + + @override + String get id => ''; +} diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml new file mode 100644 index 000000000..cb1046d5a --- /dev/null +++ b/cw_ethereum/pubspec.yaml @@ -0,0 +1,68 @@ +name: cw_ethereum +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=2.18.2 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + web3dart: 2.3.5 + mobx: ^2.0.7+4 + bip39: ^1.0.6 + bip32: ^2.0.0 + ed25519_hd_key: ^2.2.0 + hex: ^0.2.0 + http: ^0.13.4 + shared_preferences: ^2.0.15 + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +# 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: + + # To add assets to your 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 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/cw_ethereum/test/cw_ethereum_test.dart b/cw_ethereum/test/cw_ethereum_test.dart new file mode 100644 index 000000000..72026a4c0 --- /dev/null +++ b/cw_ethereum/test/cw_ethereum_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_ethereum/cw_ethereum.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index a38c4721c..226ace6a1 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -254,6 +254,7 @@ abstract class HavenWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { final currentWalletPath = await pathForWallet(name: name, type: type); final currentCacheFile = File(currentWalletPath); diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 90cc3c279..748b65329 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -14,18 +14,18 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromMap(Map map) : id = (map['hash'] ?? '') as String, height = (map['height'] ?? 0) as int, - direction = - parseTransactionDirectionFromNumber(map['direction'] as String) ?? - TransactionDirection.incoming, + direction = map['direction'] != null + ? parseTransactionDirectionFromNumber(map['direction'] as String) + : TransactionDirection.incoming, date = DateTime.fromMillisecondsSinceEpoch( - (int.parse(map['timestamp'] as String) ?? 0) * 1000), + (int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000), isPending = parseBoolFromString(map['isPending'] as String), amount = map['amount'] as int, accountIndex = int.parse(map['accountIndex'] as String), addressIndex = map['addressIndex'] as int, confirmations = map['confirmations'] as int, key = getTxKey((map['hash'] ?? '') as String), - fee = map['fee'] as int ?? 0 { + fee = map['fee'] as int? ?? 0 { additionalInfo = { 'key': key, 'accountIndex': accountIndex, @@ -36,8 +36,7 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromRow(TransactionInfoRow row) : id = row.getHash(), height = row.blockHeight, - direction = parseTransactionDirectionFromInt(row.direction) ?? - TransactionDirection.incoming, + direction = parseTransactionDirectionFromInt(row.direction), date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), isPending = row.isPending != 0, amount = row.getAmount(), diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 104b3ebe8..ef25b6b93 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -269,6 +269,7 @@ abstract class MoneroWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { final currentWalletDirPath = await pathForWalletDir(name: name, type: type); diff --git a/howto-build-android.md b/howto-build-android.md index 4ef385b9f..d37f1b417 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -6,9 +6,9 @@ The following are the system requirements to build CakeWallet for your Android d ``` Ubuntu >= 16.04 -Android SDK 28 +Android SDK 29 or higher (better to have the latest one 33) Android NDK 17c -Flutter 2 or above +Flutter 3.7.x ``` ## Building CakeWallet on Android @@ -55,7 +55,7 @@ You may download and install the latest version of Android Studio [here](https:/ ### 3. Installing Flutter -Need to install flutter with version `3.x.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). +Need to install flutter with version `3.7.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). ### 4. Verify Installations @@ -66,9 +66,9 @@ Verify that the Android toolchain, Flutter, and Android Studio have been correct 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.x.x, on Linux, locale en_US.UTF-8) -[✓] Android toolchain - develop for Android devices (Android SDK version 28) -[✓] Android Studio (version 4.0) +[✓] Flutter (Channel stable, 3.7.x, on Linux, locale en_US.UTF-8) +[✓] Android toolchain - develop for Android devices (Android SDK version 29 or higher) +[✓] Android Studio (version 4.0 or higher) ``` ### 5. Generate a secure keystore for Android diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 630ecf27f..dfd3b1538 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -80,7 +80,7 @@ class CWBitcoin extends Bitcoin { isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount)) .toList(), - priority: priority != null ? priority as BitcoinTransactionPriority : null, + priority: priority as BitcoinTransactionPriority, feeRate: feeRate); @override diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 2b3056343..f2a235363 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) @@ -14,6 +15,9 @@ class AddressValidator extends TextValidator { length: getLength(type)); static String getPattern(CryptoCurrency type) { + if (type is Erc20Token) { + return '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}\$'; @@ -56,6 +60,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zrx: case CryptoCurrency.dydx: case CryptoCurrency.steth: + case CryptoCurrency.shib: return '0x[0-9a-zA-Z]'; case CryptoCurrency.xrp: return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; @@ -116,17 +121,14 @@ class AddressValidator extends TextValidator { } static List? getLength(CryptoCurrency type) { + if (type is Erc20Token) { + return [42]; + } switch (type) { case CryptoCurrency.xmr: return null; case CryptoCurrency.ada: return null; - case CryptoCurrency.avaxc: - return [42]; - case CryptoCurrency.bch: - return [42]; - case CryptoCurrency.bnb: - return [42]; case CryptoCurrency.btc: return null; case CryptoCurrency.dash: @@ -166,6 +168,10 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zrx: case CryptoCurrency.dydx: case CryptoCurrency.steth: + case CryptoCurrency.shib: + case CryptoCurrency.avaxc: + case CryptoCurrency.bch: + case CryptoCurrency.bnb: return [42]; case CryptoCurrency.ltc: return [34, 43, 63]; @@ -203,11 +209,8 @@ class AddressValidator extends TextValidator { case CryptoCurrency.xusd: return [98, 99, 106]; case CryptoCurrency.btt: - return [34]; case CryptoCurrency.bttc: - return [34]; case CryptoCurrency.doge: - return [34]; case CryptoCurrency.firo: return [34]; case CryptoCurrency.hbar: @@ -258,6 +261,8 @@ class AddressValidator extends TextValidator { 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: + return '0x[0-9a-zA-Z]{42}'; default: return null; } diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 3f3eedd57..a109b75cd 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -240,6 +240,9 @@ class BackupService { data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?; final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?; + final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; + final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; + final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); @@ -349,6 +352,15 @@ class BackupService { PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, shouldRequireTOTP2FAForAllSecurityAndBackupSettings); + if (sortBalanceTokensBy != null) + await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); + + if (pinNativeTokenAtTop != null) + await _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop); + + if (useEtherscan != null) + await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); + await preferencesFile.delete(); } @@ -492,6 +504,12 @@ class BackupService { _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets), PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), + PreferencesKey.sortBalanceBy: + _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), + PreferencesKey.pinNativeTokenAtTop: + _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), + PreferencesKey.useEtherscan: + _sharedPreferences.getBool(PreferencesKey.useEtherscan), }; return json.encode(preferences); diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 11aef1374..9690c430a 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -5,21 +5,20 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; - const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com'; const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion'; const _fiatApiPath = '/v2/rates'; Future _fetchPrice(Map args) async { - final crypto = args['crypto'] as CryptoCurrency; - final fiat = args['fiat'] as FiatCurrency; + final crypto = args['crypto'] as String; + final fiat = args['fiat'] as String; final torOnly = args['torOnly'] as bool; final Map queryParams = { 'interval_count': '1', - 'base': crypto.toString(), - 'quote': fiat.toString(), - 'key' : secrets.fiatApiKey, + 'base': crypto, + 'quote': fiat, + 'key': secrets.fiatApiKey, }; double price = 0.0; @@ -52,7 +51,11 @@ Future _fetchPrice(Map args) async { } Future _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async => - compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto, 'torOnly': torOnly}); + compute(_fetchPrice, { + 'fiat': fiat.toString(), + 'crypto': crypto.toString(), + 'torOnly': torOnly, + }); class FiatConversionService { static Future fetchPrice({ diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index fe9a25f85..eba1bbda4 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; @@ -25,6 +26,8 @@ class SeedValidator extends Validator { return monero!.getMoneroWordList(language); case WalletType.haven: return haven!.getMoneroWordList(language); + case WalletType.ethereum: + return ethereum!.getEthereumWordList(language); default: return []; } diff --git a/lib/di.dart b/lib/di.dart index f287b0e9a..c27659e7a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/core/yat_service.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/receive_page_option.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; @@ -16,6 +17,8 @@ import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; +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/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; @@ -40,6 +43,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; @@ -70,6 +74,7 @@ import 'package:cake_wallet/view_model/advanced_privacy_settings_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:cw_core/erc20_token.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -239,9 +244,9 @@ Future setup({ getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); } - final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty ?? false) && - (secrets.wyreApiKey.isNotEmpty ?? false) && - (secrets.wyreAccountId.isNotEmpty ?? false); + final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) && + (secrets.wyreApiKey.isNotEmpty) && + (secrets.wyreAccountId.isNotEmpty); final settingsStore = await SettingsStoreBase.load( nodeSource: _nodeSource, @@ -638,7 +643,7 @@ Future setup({ }); getIt.registerFactory(() { - return PrivacySettingsViewModel(getIt.get()); + return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!); }); getIt.registerFactory(() { @@ -745,6 +750,8 @@ Future setup({ return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); case WalletType.litecoin: return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); + case WalletType.ethereum: + return ethereum!.createEthereumWalletService(_walletInfoSource); default: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } @@ -787,8 +794,8 @@ Future setup({ transactionDetailsViewModel: getIt.get(param1: transactionInfo))); - getIt.registerFactoryParam( - (param1, _) => NewWalletTypePage(onTypeSelected: param1)); + getIt.registerFactoryParam( + (param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true)); getIt.registerFactoryParam( (WalletType type, _) => PreSeedPage(type)); @@ -1034,5 +1041,19 @@ Future setup({ getIt.registerFactoryParam( (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get())); + getIt.registerFactoryParam((balanceViewModel, _) => + HomeSettingsPage(getIt.get(param1: balanceViewModel))); + + getIt.registerFactoryParam( + (balanceViewModel, _) => HomeSettingsViewModel(getIt.get(), balanceViewModel)); + + getIt.registerFactoryParam>( + (homeSettingsViewModel, arguments) => EditTokenPage( + homeSettingsViewModel: homeSettingsViewModel, + erc20token: arguments['token'] as Erc20Token?, + initialContractAddress: arguments['contractAddress'] as String?, + ), + ); + _isSetupFinished = true; } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 77298c2b5..b4cb23131 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -26,6 +26,7 @@ const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; +const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; Future defaultSettingsMigration( {required int version, @@ -157,6 +158,12 @@ Future defaultSettingsMigration( case 20: await migrateExchangeStatus(sharedPreferences); break; + case 21: + await addEthereumNodeList(nodes: nodes); + await changeEthereumCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + break; + default: break; } @@ -242,6 +249,12 @@ Node? getHavenDefaultNode({required Box nodes}) { ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); } +Node? getEthereumDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull( + (Node node) => node.uriRaw == ethereumDefaultNodeUri) + ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); +} + Node getMoneroDefaultNode({required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; @@ -438,6 +451,8 @@ Future checkCurrentNodes( .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final currentHavenNodeId = sharedPreferences .getInt(PreferencesKey.currentHavenNodeIdKey); + final currentEthereumNodeId = sharedPreferences + .getInt(PreferencesKey.currentEthereumNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull( (node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull( @@ -446,6 +461,8 @@ Future checkCurrentNodes( (node) => node.key == currentLitecoinElectrumSeverId); final currentHavenNodeServer = nodeSource.values.firstWhereOrNull( (node) => node.key == currentHavenNodeId); + final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentEthereumNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = @@ -479,6 +496,13 @@ Future checkCurrentNodes( await sharedPreferences.setInt( PreferencesKey.currentHavenNodeIdKey, node.key as int); } + + if (currentEthereumNodeServer == null) { + final node = Node(uri: ethereumDefaultNodeUri, type: WalletType.ethereum); + await nodeSource.add(node); + await sharedPreferences.setInt( + PreferencesKey.currentEthereumNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -522,8 +546,26 @@ Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { return; } - await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled + await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled ? ExchangeApiMode.disabled.raw : ExchangeApiMode.enabled.raw); - + await sharedPreferences.remove(PreferencesKey.disableExchangeKey); } + +Future addEthereumNodeList({required Box nodes}) async { + final nodeList = await loadDefaultEthereumNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeEthereumCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, + required Box nodes}) async { + final node = getEthereumDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); +} diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 0cf3cead4..d6a7445f9 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -46,6 +46,7 @@ class MainActions { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.ethereum: if (viewModel.isEnabledBuyAction) { final uri = getIt.get().requestUrl(); if (DeviceInfo.instance.isMobile) { @@ -116,6 +117,7 @@ class MainActions { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.ethereum: if (viewModel.isEnabledSellAction) { final moonPaySellProvider = MoonPaySellProvider(); final uri = await moonPaySellProvider.requestUrl( diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 58847ccfa..b06351a79 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -70,6 +70,22 @@ Future> loadDefaultHavenNodes() async { return nodes; } +Future> loadDefaultEthereumNodes() async { + final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + node.type = WalletType.ethereum; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 027e05f55..62c47ea02 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -5,6 +5,7 @@ class PreferencesKey { static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentHavenNodeIdKey = 'current_node_id_xhv'; + static const currentEthereumNodeIdKey = 'current_node_id_eth'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; @@ -31,6 +32,7 @@ class PreferencesKey { static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin'; static const havenTransactionPriority = 'current_fee_priority_haven'; static const litecoinTransactionPriority = 'current_fee_priority_litecoin'; + static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; @@ -38,6 +40,9 @@ class PreferencesKey { static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastPopupDate = 'last_popup_date'; static const lastAppReviewDate = 'last_app_review_date'; + static const sortBalanceBy = 'sort_balance_by'; + static const pinNativeTokenAtTop = 'pin_native_token_at_top'; + static const useEtherscan = 'use_etherscan'; static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 927ab8803..eb9417763 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -14,6 +15,8 @@ List priorityForWalletType(WalletType type) { return bitcoin!.getLitecoinTransactionPriorities(); case WalletType.haven: return haven!.getTransactionPriorities(); + case WalletType.ethereum: + return ethereum!.getTransactionPriorities(); default: return []; } diff --git a/lib/entities/sort_balance_types.dart b/lib/entities/sort_balance_types.dart new file mode 100644 index 000000000..5db64884e --- /dev/null +++ b/lib/entities/sort_balance_types.dart @@ -0,0 +1,19 @@ +import 'package:cake_wallet/generated/i18n.dart'; + +enum SortBalanceBy { + FiatBalance, + GrossBalance, + Alphabetical; + + @override + String toString() { + switch (this) { + case SortBalanceBy.FiatBalance: + return S.current.fiat_balance; + case SortBalanceBy.GrossBalance: + return S.current.gross_balance; + case SortBalanceBy.Alphabetical: + return S.current.alphabetical; + } + } +} \ No newline at end of file diff --git a/lib/entities/template.dart b/lib/entities/template.dart index 8224ecdd8..6955136e0 100644 --- a/lib/entities/template.dart +++ b/lib/entities/template.dart @@ -55,5 +55,5 @@ class Template extends HiveObject { String get amount => amountRaw ?? ''; - List