diff --git a/cw_core/lib/address_info.dart b/cw_core/lib/address_info.dart new file mode 100644 index 000000000..63dc023ab --- /dev/null +++ b/cw_core/lib/address_info.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'address_info.g.dart'; + +@HiveType(typeId: ADDRESS_INFO_TYPE_ID) +class AddressInfo extends HiveObject { + AddressInfo({required this.address, this.accountIndex, required this.label}); + + static const typeId = ADDRESS_INFO_TYPE_ID; + static const boxName = 'AddressInfo'; + + @HiveField(0) + int? accountIndex; + + @HiveField(1, defaultValue: '') + String address; + + @HiveField(2, defaultValue: '') + String label; +} diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index 0961182bc..950f39e1f 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -9,5 +9,5 @@ const EXCHANGE_TEMPLATE_TYPE_ID = 7; const ORDER_TYPE_ID = 8; const UNSPENT_COINS_INFO_TYPE_ID = 9; const ANONPAY_INVOICE_INFO_TYPE_ID = 10; - +const ADDRESS_INFO_TYPE_ID = 11; const ERC20_TOKEN_TYPE_ID = 12; diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index a34101a88..27b5468c5 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,8 +1,10 @@ +import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_info.dart'; abstract class WalletAddresses { WalletAddresses(this.walletInfo) - : addressesMap = {}; + : addressesMap = {}, + addressInfos = {}; final WalletInfo walletInfo; @@ -12,6 +14,10 @@ abstract class WalletAddresses { Map addressesMap; + Map> addressInfos; + + Set usedAddresses = {}; + Future init(); Future updateAddressesInBox(); @@ -20,6 +26,8 @@ abstract class WalletAddresses { try { walletInfo.address = address; walletInfo.addresses = addressesMap; + walletInfo.addressInfos = addressInfos; + walletInfo.usedAddresses = usedAddresses.toList(); if (walletInfo.isInBox) { await walletInfo.save(); diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 5bc5ef914..e24fdb4b0 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -52,6 +52,10 @@ abstract class WalletBase< late HistoryType transactionHistory; + set isEnabledAutoGenerateSubaddress(bool value) {} + + bool get isEnabledAutoGenerateSubaddress => false; + Future connectToNode({required Node node}); Future startSync(); diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 6b3fa9e98..210adb9a4 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:cw_core/address_info.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; @@ -72,6 +73,12 @@ class WalletInfo extends HiveObject { @HiveField(13) bool? showIntroCakePayCard; + @HiveField(14) + Map>? addressInfos; + + @HiveField(15) + List? usedAddresses; + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 226ace6a1..e639be4b9 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -12,8 +12,7 @@ import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_haven/api/structs/pending_transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_haven/api/transaction_history.dart' - as haven_transaction_history; +import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history; //import 'package:cw_haven/wallet.dart'; import 'package:cw_haven/api/wallet.dart' as haven_wallet; import 'package:cw_haven/api/transaction_history.dart' as transaction_history; @@ -37,8 +36,8 @@ const moneroBlockSize = 1000; class HavenWallet = HavenWalletBase with _$HavenWallet; -abstract class HavenWalletBase extends WalletBase with Store { +abstract class HavenWalletBase + extends WalletBase with Store { HavenWalletBase({required WalletInfo walletInfo}) : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)), _isTransactionUpdating = false, @@ -47,8 +46,7 @@ abstract class HavenWalletBase extends WalletBase walletAddresses.account, - (Account? account) { + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { if (account == null) { return; } @@ -96,14 +94,12 @@ abstract class HavenWalletBase extends WalletBase await save()); + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); } @override @@ -115,7 +111,7 @@ abstract class HavenWalletBase extends WalletBase connectToNode({required Node node}) async { try { @@ -170,26 +166,25 @@ abstract class HavenWalletBase extends WalletBase item.sendAll - || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw HavenTransactionCreationException( + 'You do not have enough coins to send this amount.'); } - final int totalAmount = outputs.fold(0, (acc, value) => - acc + (value.formattedCryptoAmount ?? 0)); + final int totalAmount = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); if (unlockedBalance < totalAmount) { - throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); + throw HavenTransactionCreationException( + 'You do not have enough coins to send this amount.'); } - final moneroOutputs = outputs.map((output) => - MoneroOutput( - address: output.address, - amount: output.cryptoAmount!.replaceAll(',', '.'))) + final moneroOutputs = outputs + .map((output) => MoneroOutput( + address: output.address, amount: output.cryptoAmount!.replaceAll(',', '.'))) .toList(); - pendingTransactionDescription = - await transaction_history.createTransactionMultDest( + pendingTransactionDescription = await transaction_history.createTransactionMultDest( outputs: moneroOutputs, priorityRaw: _credentials.priority.serialize(), accountIndex: walletAddresses.account!.id); @@ -198,12 +193,8 @@ abstract class HavenWalletBase extends WalletBase - haven_wallet.getAddress( - accountIndex: accountIndex, - addressIndex: addressIndex); + haven_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); @override Future> fetchTransactions() async { haven_transaction_history.refreshTransactions(); - return _getAllTransactions(null).fold>( - {}, - (Map acc, HavenTransactionInfo tx) { + return _getAllTransactions(null) + .fold>({}, + (Map acc, HavenTransactionInfo tx) { acc[tx.id] = tx; return acc; }); @@ -340,9 +328,9 @@ abstract class HavenWalletBase extends WalletBase _getAllTransactions(dynamic _) => haven_transaction_history - .getAllTransations() - .map((row) => HavenTransactionInfo.fromRow(row)) - .toList(); + .getAllTransations() + .map((row) => HavenTransactionInfo.fromRow(row)) + .toList(); void _setListeners() { _listener?.stop(); @@ -364,8 +352,7 @@ abstract class HavenWalletBase extends WalletBase balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id)); - Future _askForUpdateTransactionHistory() async => - await updateTransactions(); + Future _askForUpdateTransactionHistory() async => await updateTransactions(); void _onNewBlock(int height, int blocksLeft, double ptc) async { try { @@ -404,9 +390,9 @@ abstract class HavenWalletBase extends WalletBase(); + : _isRefreshing = false, + _isUpdating = false, + subaddresses = ObservableList(); + + final List _usedAddresses = []; @observable ObservableList subaddresses; @@ -49,20 +50,24 @@ abstract class MoneroSubaddressListBase with Store { subaddresses = [primary] + rest.toList(); } - return subaddresses - .map((subaddressRow) => Subaddress( + return subaddresses.map((subaddressRow) { + final hasDefaultAddressName = + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() || + subaddressRow.getLabel().toLowerCase() == 'Untitled account'.toLowerCase(); + final isPrimaryAddress = subaddressRow.getId() == 0 && hasDefaultAddressName; + return Subaddress( id: subaddressRow.getId(), address: subaddressRow.getAddress(), - label: subaddressRow.getId() == 0 && - subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() - ? 'Primary address' - : subaddressRow.getLabel())) - .toList(); + label: isPrimaryAddress + ? 'Primary address' + : hasDefaultAddressName + ? '' + : subaddressRow.getLabel()); + }).toList(); } Future addSubaddress({required int accountIndex, required String label}) async { - await subaddress_list.addSubaddress( - accountIndex: accountIndex, label: label); + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); update(accountIndex: accountIndex); } @@ -88,4 +93,59 @@ abstract class MoneroSubaddressListBase with Store { rethrow; } } + + Future updateWithAutoGenerate({ + required int accountIndex, + required String defaultLabel, + required List usedAddresses, + }) async { + _usedAddresses.addAll(usedAddresses); + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(accountIndex: accountIndex); + subaddresses.clear(); + final newSubAddresses = + await _getAllUnusedAddresses(accountIndex: accountIndex, label: defaultLabel); + subaddresses.addAll(newSubAddresses); + } catch (e) { + rethrow; + } finally { + _isUpdating = false; + } + } + + Future> _getAllUnusedAddresses( + {required int accountIndex, required String label}) async { + final allAddresses = subaddress_list.getAllSubaddresses(); + + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last.getAddress())) { + final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); + if (!isAddressUnused) { + return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); + } + } + + return allAddresses + .map((subaddressRow) => Subaddress( + id: subaddressRow.getId(), + address: subaddressRow.getAddress(), + label: subaddressRow.getId() == 0 && + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() + ? 'Primary address' + : subaddressRow.getLabel())) + .toList(); + } + + Future _newSubaddress({required int accountIndex, required String label}) async { + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); + + return subaddress_list + .getAllSubaddresses() + .where((subaddressRow) => !_usedAddresses.contains(subaddressRow.getAddress())) + .isNotEmpty; + } } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 76563310e..39c0604a3 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -48,12 +48,14 @@ abstract class MoneroWalletBase extends WalletBase walletAddresses.account, (Account? account) { if (account == null) { return; @@ -64,7 +66,11 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { + _updateSubAddress(enabled, account: walletAddresses.account); }); } @@ -73,7 +79,11 @@ abstract class MoneroWalletBase extends WalletBase unspentCoinsInfo; @override - MoneroWalletAddresses walletAddresses; + late MoneroWalletAddresses walletAddresses; + + @override + @observable + bool isEnabledAutoGenerateSubaddress; @override @observable @@ -287,6 +297,14 @@ abstract class MoneroWalletBase extends WalletBase save() async { + await walletAddresses.updateUsedSubaddress(); + + if (isEnabledAutoGenerateSubaddress) { + walletAddresses.updateUnusedSubaddress( + accountIndex: walletAddresses.account?.id ?? 0, + defaultLabel: walletAddresses.account?.label ?? ''); + } + await walletAddresses.updateAddressesInBox(); await backupWalletFiles(name); await monero_wallet.store(); @@ -610,4 +628,15 @@ abstract class MoneroWalletBase extends WalletBase init() async { accountList.update(); account = accountList.accounts.first; - updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -46,11 +50,15 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { final _subaddressList = MoneroSubaddressList(); addressesMap.clear(); + addressInfos.clear(); accountList.accounts.forEach((account) { _subaddressList.update(accountIndex: account.id); _subaddressList.subaddresses.forEach((subaddress) { addressesMap[subaddress.address] = subaddress.label; + addressInfos[account.id] ??= []; + addressInfos[account.id]?.add(AddressInfo( + address: subaddress.address, label: subaddress.label, accountIndex: account.id)); }); }); @@ -62,14 +70,14 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { bool validate() { accountList.update(); - final accountListLength = accountList.accounts.length ?? 0; + final accountListLength = accountList.accounts.length; if (accountListLength <= 0) { return false; } subaddressList.update(accountIndex: accountList.accounts.first.id); - final subaddressListLength = subaddressList.subaddresses.length ?? 0; + final subaddressListLength = subaddressList.subaddresses.length; if (subaddressListLength <= 0) { return false; @@ -83,4 +91,24 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { subaddress = subaddressList.subaddresses.first; address = subaddress!.address; } -} \ No newline at end of file + + Future updateUsedSubaddress() async { + final transactions = _moneroTransactionHistory.transactions.values.toList(); + + transactions.forEach((element) { + final accountIndex = element.accountIndex; + final addressIndex = element.addressIndex; + usedAddresses.add(getAddress(accountIndex: accountIndex, addressIndex: addressIndex)); + }); + } + + Future updateUnusedSubaddress( + {required int accountIndex, required String defaultLabel}) async { + await subaddressList.updateWithAutoGenerate( + accountIndex: accountIndex, + defaultLabel: defaultLabel, + usedAddresses: usedAddresses.toList()); + subaddress = subaddressList.subaddresses.last; + address = subaddress!.address; + } +} diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 6476891ed..3f430b7e9 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -246,6 +246,7 @@ class BackupService { final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; final syncAll = data[PreferencesKey.syncAllKey] as bool?; final syncMode = data[PreferencesKey.syncModeKey] as int?; + final autoGenerateSubaddressStatus = data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?; await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); @@ -296,6 +297,9 @@ class BackupService { if (fiatApiMode != null) await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); + if (autoGenerateSubaddressStatus != null) + await _sharedPreferences.setInt(PreferencesKey.autoGenerateSubaddressStatusKey, + autoGenerateSubaddressStatus); if (currentPinLength != null) await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); @@ -523,6 +527,8 @@ class BackupService { _sharedPreferences.getInt(PreferencesKey.syncModeKey), PreferencesKey.syncAllKey: _sharedPreferences.getBool(PreferencesKey.syncAllKey), + PreferencesKey.autoGenerateSubaddressStatusKey: + _sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey), }; return json.encode(preferences); diff --git a/lib/di.dart b/lib/di.dart index 80a55e1c6..d32576836 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; +import 'package:cake_wallet/entities/auto_generate_subaddress_status.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'; @@ -245,7 +246,6 @@ Future setup({ if (!_isSetupFinished) { getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); } - if (!_isSetupFinished) { getIt.registerFactory(() => BackgroundTasks()); } diff --git a/lib/entities/auto_generate_subaddress_status.dart b/lib/entities/auto_generate_subaddress_status.dart new file mode 100644 index 000000000..6d6cc406c --- /dev/null +++ b/lib/entities/auto_generate_subaddress_status.dart @@ -0,0 +1,13 @@ + +enum AutoGenerateSubaddressStatus { + initialized(1), + enabled(2), + disabled(3); + + const AutoGenerateSubaddressStatus(this.value); + final int value; + + static AutoGenerateSubaddressStatus deserialize({required int raw}) => + AutoGenerateSubaddressStatus.values.firstWhere((e) => e.value == raw); + +} \ No newline at end of file diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index c50629c1b..7b4d3d0dc 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -50,6 +50,7 @@ class PreferencesKey { '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; static const exchangeProvidersSelection = 'exchange-providers-selection'; + static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status'; static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const lastSeenAppVersion = 'last_seen_app_version'; diff --git a/lib/main.dart b/lib/main.dart index db5335ac1..62d18708e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/locales/locale.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cw_core/address_info.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:flutter/foundation.dart'; @@ -89,6 +90,10 @@ Future initializeAppConfigs() async { CakeHive.registerAdapter(TradeAdapter()); } + if (!CakeHive.isAdapterRegistered(AddressInfo.typeId)) { + CakeHive.registerAdapter(AddressInfoAdapter()); + } + if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) { CakeHive.registerAdapter(WalletInfoAdapter()); } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 89f39d5f9..95406fb40 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -21,36 +22,36 @@ ReactionDisposer? _onCurrentWalletChangeReaction; ReactionDisposer? _onCurrentWalletChangeFiatRateUpdateReaction; //ReactionDisposer _onCurrentWalletAddressChangeReaction; -void startCurrentWalletChangeReaction(AppStore appStore, - SettingsStore settingsStore, FiatConversionStore fiatConversionStore) { +void startCurrentWalletChangeReaction( + AppStore appStore, SettingsStore settingsStore, FiatConversionStore fiatConversionStore) { _onCurrentWalletChangeReaction?.reaction.dispose(); _onCurrentWalletChangeFiatRateUpdateReaction?.reaction.dispose(); //_onCurrentWalletAddressChangeReaction?.reaction?dispose(); //_onCurrentWalletAddressChangeReaction = reaction((_) => appStore.wallet.walletAddresses.address, - //(String address) async { - //if (address == appStore.wallet.walletInfo.yatLastUsedAddress) { - // return; - //} + //(String address) async { + //if (address == appStore.wallet.walletInfo.yatLastUsedAddress) { + // return; + //} - //try { - // final yatStore = getIt.get(); - // await updateEmojiIdAddress( - // appStore.wallet.walletInfo.yatEmojiId, - // appStore.wallet.walletAddresses.address, - // yatStore.apiKey, - // appStore.wallet.type - // ); - // appStore.wallet.walletInfo.yatLastUsedAddress = address; - // await appStore.wallet.walletInfo.save(); - //} catch (e) { - // print(e.toString()); - //} + //try { + // final yatStore = getIt.get(); + // await updateEmojiIdAddress( + // appStore.wallet.walletInfo.yatEmojiId, + // appStore.wallet.walletAddresses.address, + // yatStore.apiKey, + // appStore.wallet.type + // ); + // appStore.wallet.walletInfo.yatLastUsedAddress = address; + // await appStore.wallet.walletInfo.save(); + //} catch (e) { + // print(e.toString()); + //} //}); - _onCurrentWalletChangeReaction = reaction((_) => appStore.wallet, (WalletBase< - Balance, TransactionHistoryBase, TransactionInfo>? - wallet) async { + _onCurrentWalletChangeReaction = reaction((_) => appStore.wallet, + (WalletBase, TransactionInfo>? + wallet) async { try { if (wallet == null) { return; @@ -59,11 +60,13 @@ void startCurrentWalletChangeReaction(AppStore appStore, final node = settingsStore.getCurrentNode(wallet.type); startWalletSyncStatusChangeReaction(wallet, fiatConversionStore); startCheckConnectionReaction(wallet, settingsStore); + await getIt.get().setString(PreferencesKey.currentWalletName, wallet.name); await getIt .get() - .setString(PreferencesKey.currentWalletName, wallet.name); - await getIt.get().setInt( - PreferencesKey.currentWalletType, serializeToInt(wallet.type)); + .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); + if (wallet.type == WalletType.monero) { + _setAutoGenerateSubaddressStatus(wallet, settingsStore); + } await wallet.connectToNode(node: node); if (wallet.type == WalletType.haven) { @@ -82,9 +85,8 @@ void startCurrentWalletChangeReaction(AppStore appStore, } }); - _onCurrentWalletChangeFiatRateUpdateReaction = - reaction((_) => appStore.wallet, (WalletBase, TransactionInfo>? + _onCurrentWalletChangeFiatRateUpdateReaction = reaction((_) => appStore.wallet, + (WalletBase, TransactionInfo>? wallet) async { try { if (wallet == null || settingsStore.fiatApiMode == FiatApiMode.disabled) { @@ -92,11 +94,10 @@ void startCurrentWalletChangeReaction(AppStore appStore, } fiatConversionStore.prices[wallet.currency] = 0; - fiatConversionStore.prices[wallet.currency] = - await FiatConversionService.fetchPrice( - crypto: wallet.currency, - fiat: settingsStore.fiatCurrency, - torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); + fiatConversionStore.prices[wallet.currency] = await FiatConversionService.fetchPrice( + crypto: wallet.currency, + fiat: settingsStore.fiatCurrency, + torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); if (wallet.type == WalletType.ethereum) { final currencies = @@ -116,3 +117,17 @@ void startCurrentWalletChangeReaction(AppStore appStore, } }); } + +void _setAutoGenerateSubaddressStatus( + WalletBase, TransactionInfo> wallet, + SettingsStore settingsStore, +) async { + final walletHasAddresses = await wallet.walletAddresses.addressesMap.length > 1; + if (settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized && + walletHasAddresses) { + settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled; + } + wallet.isEnabledAutoGenerateSubaddress = + settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled || + settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized; +} diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index 236087595..2f18ef634 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -1,8 +1,10 @@ import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; @@ -24,7 +26,6 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; @@ -174,7 +175,11 @@ class AddressPage extends BasePage { Observer(builder: (_) { if (addressListViewModel.hasAddressList) { return GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.receive), + onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled + ? await showPopUp( + context: context, + builder: (_) => getIt.get()) + : Navigator.of(context).pushNamed(Routes.receive), child: Container( height: 50, padding: EdgeInsets.only(left: 24, right: 12), @@ -193,17 +198,26 @@ class AddressPage extends BasePage { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Observer( - builder: (_) => Text( - addressListViewModel.hasAccounts - ? S.of(context).accounts_subaddresses - : S.of(context).addresses, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) + builder: (_) { + String label = addressListViewModel.hasAccounts + ? S.of(context).accounts_subaddresses + : S.of(context).addresses; + + if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) { + label = addressListViewModel.hasAccounts + ? S.of(context).accounts + : S.of(context).account; + } + return Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) .extension()! .textColor), - )), + ); + },), Icon( Icons.arrow_forward_ios, size: 14, @@ -213,7 +227,7 @@ class AddressPage extends BasePage { ), ), ); - } else if (addressListViewModel.showElectrumAddressDisclaimer) { + } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled || addressListViewModel.showElectrumAddressDisclaimer) { return Text(S.of(context).electrum_address_disclaimer, textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/src/screens/monero_accounts/widgets/account_tile.dart b/lib/src/screens/monero_accounts/widgets/account_tile.dart index fa8221513..3e428f355 100644 --- a/lib/src/screens/monero_accounts/widgets/account_tile.dart +++ b/lib/src/screens/monero_accounts/widgets/account_tile.dart @@ -5,13 +5,14 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:cake_wallet/generated/i18n.dart'; class AccountTile extends StatelessWidget { - AccountTile( - {required this.isCurrent, - required this.accountName, - this.accountBalance, - required this.currency, - required this.onTap, - required this.onEdit}); + AccountTile({ + required this.isCurrent, + required this.accountName, + this.accountBalance, + required this.currency, + required this.onTap, + required this.onEdit, + }); final bool isCurrent; final String accountName; diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 74e33b87a..3adad4379 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -73,7 +73,7 @@ class RestoreOptionsPage extends BasePage { await restoreFromQRViewModel.create(restoreWallet: restoreWallet); if (restoreFromQRViewModel.state is FailureState) { _onWalletCreateFailure(context, - 'Create wallet state: ${restoreFromQRViewModel.state.runtimeType.toString()}'); + 'Create wallet state: ${(restoreFromQRViewModel.state as FailureState).error}'); } } catch (e) { _onWalletCreateFailure(context, e.toString()); diff --git a/lib/src/screens/restore/widgets/backup_file_button.dart b/lib/src/screens/restore/widgets/backup_file_button.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index ae6bfe6c8..e953fd4ee 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -50,6 +50,14 @@ class PrivacyPage extends BasePage { onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setShouldSaveRecipientAddress(value); }), + if (_privacySettingsViewModel.isAutoGenerateSubaddressesVisible) + SettingsSwitcherCell( + title: S.current.auto_generate_subaddresses, + value: _privacySettingsViewModel.isAutoGenerateSubaddressesEnabled, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setAutoGenerateSubaddresses(value); + }, + ), if (DeviceInfo.instance.isMobile) SettingsSwitcherCell( title: S.current.prevent_screenshots, diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 703c7d73e..f512063a4 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/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; @@ -42,6 +43,7 @@ abstract class SettingsStoreBase with Store { required FiatCurrency initialFiatCurrency, required BalanceDisplayMode initialBalanceDisplayMode, required bool initialSaveRecipientAddress, + required AutoGenerateSubaddressStatus initialAutoGenerateSubaddressStatus, required bool initialAppSecure, required bool initialDisableBuy, required bool initialDisableSell, @@ -87,6 +89,7 @@ abstract class SettingsStoreBase with Store { fiatCurrency = initialFiatCurrency, balanceDisplayMode = initialBalanceDisplayMode, shouldSaveRecipientAddress = initialSaveRecipientAddress, + autoGenerateSubaddressStatus = initialAutoGenerateSubaddressStatus, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, selectedCake2FAPreset = initialCake2FAPresetOptions, @@ -197,6 +200,11 @@ abstract class SettingsStoreBase with Store { (bool disableSell) => sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell)); + reaction( + (_) => autoGenerateSubaddressStatus, + (AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt( + PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus.value)); + reaction( (_) => fiatApiMode, (FiatApiMode mode) => @@ -337,6 +345,7 @@ abstract class SettingsStoreBase with Store { static const defaultPinLength = 4; static const defaultActionsMode = 11; static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes; + static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; @observable FiatCurrency fiatCurrency; @@ -359,6 +368,9 @@ abstract class SettingsStoreBase with Store { @observable bool shouldSaveRecipientAddress; + @observable + AutoGenerateSubaddressStatus autoGenerateSubaddressStatus; + @observable bool isAppSecure; @@ -602,7 +614,12 @@ abstract class SettingsStoreBase with Store { final packageInfo = await PackageInfo.fromPlatform(); final deviceName = await _getDeviceName() ?? ''; final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; + final generateSubaddresses = + sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); + final autoGenerateSubaddressStatus = generateSubaddresses != null + ? AutoGenerateSubaddressStatus.deserialize(raw: generateSubaddresses) + : defaultAutoGenerateSubaddressStatus; final nodes = {}; if (moneroNode != null) { @@ -640,6 +657,7 @@ abstract class SettingsStoreBase with Store { initialFiatCurrency: currentFiatCurrency, initialBalanceDisplayMode: currentBalanceDisplayMode, initialSaveRecipientAddress: shouldSaveRecipientAddress, + initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, initialAppSecure: isAppSecure, initialDisableBuy: disableBuy, initialDisableSell: disableSell, @@ -709,6 +727,13 @@ abstract class SettingsStoreBase with Store { priority[WalletType.ethereum]!; } + final generateSubaddresses = + sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); + + autoGenerateSubaddressStatus = generateSubaddresses != null + ? AutoGenerateSubaddressStatus.deserialize(raw: generateSubaddresses) + : defaultAutoGenerateSubaddressStatus; + balanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); shouldSaveRecipientAddress = @@ -719,8 +744,6 @@ abstract class SettingsStoreBase with Store { numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; - sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? - shouldSaveRecipientAddress; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy; disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell; 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 c99984ebc..93b06c378 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -10,6 +11,7 @@ import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/utils/mobx.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:collection/collection.dart'; part 'contact_list_view_model.g.dart'; @@ -20,12 +22,26 @@ abstract class ContactListViewModelBase with Store { ContactListViewModelBase(this.contactSource, this.walletInfoSource, this._currency, this.settingsStore) : contacts = ObservableList(), - walletContacts = [] { + walletContacts = [], + isAutoGenerateEnabled = + settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { - if (info.addresses?.isNotEmpty ?? false) { - info.addresses?.forEach((address, label) { - final name = label.isNotEmpty ? info.name + ' ($label)' : info.name; - + if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) { + info.addressInfos!.forEach((key, value) { + final nextUnusedAddress = value.firstWhereOrNull( + (addressInfo) => !(info.usedAddresses?.contains(addressInfo.address) ?? false)); + if (nextUnusedAddress != null) { + final name = _createName(info.name, nextUnusedAddress.label); + walletContacts.add(WalletContact( + nextUnusedAddress.address, + name, + walletTypeToCryptoCurrency(info.type), + )); + } + }); + } else if (info.addresses?.isNotEmpty == true) { + info.addresses!.forEach((address, label) { + final name = _createName(info.name, label); walletContacts.add(WalletContact( address, name, @@ -40,6 +56,11 @@ abstract class ContactListViewModelBase with Store { initialFire: true); } + String _createName(String walletName, String label) { + return label.isNotEmpty ? '$walletName ($label)' : walletName; + } + + final bool isAutoGenerateEnabled; final Box contactSource; final Box walletInfoSource; final ObservableList contacts; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 7035130c0..009fe0350 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; @@ -235,6 +236,10 @@ abstract class DashboardViewModelBase with Store { @computed double get price => balanceViewModel.price; + @computed + bool get isAutoGenerateSubaddressesEnabled => + settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled; + @computed List get items { final _items = []; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 5dbfd61dd..27ce919df 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,6 +1,10 @@ +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; @@ -14,11 +18,27 @@ abstract class PrivacySettingsViewModelBase with Store { PrivacySettingsViewModelBase(this._settingsStore, this._wallet); final SettingsStore _settingsStore; - final WalletBase _wallet; + final WalletBase, TransactionInfo> _wallet; @computed ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus; + @computed + bool get isAutoGenerateSubaddressesEnabled => + _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled; + + @action + void setAutoGenerateSubaddresses(bool value) { + _wallet.isEnabledAutoGenerateSubaddress = value; + if (value) { + _settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.enabled; + } else { + _settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled; + } + } + + bool get isAutoGenerateSubaddressesVisible => _wallet.type == WalletType.monero; + @computed bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 5bd8906fe..ae3f669ca 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -682,5 +682,7 @@ "support_title_other_links": "روابط دعم أخرى", "support_description_other_links": "انضم إلى مجتمعاتنا أو تصل إلينا شركائنا من خلال أساليب أخرى", "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", - "save_to_downloads": "ﺕﻼﻳﺰﻨﺘﻟﺍ ﻲﻓ ﻆﻔﺣ" + "save_to_downloads": "ﺕﻼﻳﺰﻨﺘﻟﺍ ﻲﻓ ﻆﻔﺣ", + "support_description_other_links": "انضم إلى مجتمعاتنا أو تصل إلينا شركائنا من خلال أساليب أخرى", + "auto_generate_subaddresses": "تلقائي توليد subddresses" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 325057aa4..ac2a08088 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -626,6 +626,7 @@ "setup_totp_recommended": "Настройка на TOTP (препоръчително)", "disable_buy": "Деактивирайте действието за покупка", "disable_sell": "Деактивирайте действието за продажба", + "auto_generate_subaddresses": "Автоматично генериране на подадреси", "cake_2fa_preset": "Торта 2FA Preset", "narrow": "Тесен", "normal": "нормално", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index a258eaa40..38e01ca6b 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -626,6 +626,7 @@ "setup_totp_recommended": "Nastavit TOTP (doporučeno)", "disable_buy": "Zakázat akci nákupu", "disable_sell": "Zakázat akci prodeje", + "auto_generate_subaddresses": "Automaticky generovat podadresy", "cake_2fa_preset": "Předvolba Cake 2FA", "narrow": "Úzký", "normal": "Normální", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index b315ddb56..afc5fba82 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -640,6 +640,7 @@ "high_contrast_theme": "Kontrastreiches Thema", "matrix_green_dark_theme": "Matrix Green Dark Theme", "monero_light_theme": "Monero Light-Thema", + "auto_generate_subaddresses": "Unteradressen automatisch generieren", "cake_2fa_preset" : "Cake 2FA-Voreinstellung", "narrow": "Eng", "normal": "Normal", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 9e8e3788c..4698701de 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -568,6 +568,7 @@ "privacy": "Privacy", "display_settings": "Display settings", "other_settings": "Other settings", + "auto_generate_subaddresses": "Auto generate subaddresses", "require_pin_after": "Require PIN after", "always": "Always", "minutes_to_pin_code": "${minute} minutes", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 3c0abce48..8a4519255 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -640,6 +640,7 @@ "high_contrast_theme": "Tema de alto contraste", "matrix_green_dark_theme": "Matrix verde oscuro tema", "monero_light_theme": "Tema ligero de Monero", + "auto_generate_subaddresses": "Generar subdirecciones automáticamente", "cake_2fa_preset": "Pastel 2FA preestablecido", "narrow": "Angosto", "normal": "Normal", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 1449d6370..357a73110 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -640,6 +640,7 @@ "high_contrast_theme": "Thème à contraste élevé", "matrix_green_dark_theme": "Thème Matrix Green Dark", "monero_light_theme": "Thème de lumière Monero", + "auto_generate_subaddresses": "Générer automatiquement des sous-adresses", "cake_2fa_preset": "Gâteau 2FA prédéfini", "narrow": "Étroit", "normal": "Normal", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index e3542e9ac..d344bfb15 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -618,6 +618,7 @@ "high_contrast_theme": "Babban Jigon Kwatance", "matrix_green_dark_theme": "Matrix Green Dark Jigo", "monero_light_theme": "Jigon Hasken Monero", + "auto_generate_subaddresses": "Saɓaƙa subaddresses ta kai tsaye", "cake_2fa_preset": "Cake 2FA saiti", "narrow": "kunkuntar", "normal": "Na al'ada", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index ca3e79bd0..e9ee219fe 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -640,6 +640,7 @@ "high_contrast_theme": "उच्च कंट्रास्ट थीम", "matrix_green_dark_theme": "मैट्रिक्स ग्रीन डार्क थीम", "monero_light_theme": "मोनेरो लाइट थीम", + "auto_generate_subaddresses": "स्वचालित रूप से उप-पते उत्पन्न करें", "cake_2fa_preset": "केक 2एफए प्रीसेट", "narrow": "सँकरा", "normal": "सामान्य", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index e19d32ea1..91176248d 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -640,6 +640,7 @@ "high_contrast_theme": "Tema visokog kontrasta", "matrix_green_dark_theme": "Matrix Green Dark Theme", "monero_light_theme": "Monero lagana tema", + "auto_generate_subaddresses": "Automatski generirajte podadrese", "cake_2fa_preset": "Cake 2FA Preset", "narrow": "Usko", "normal": "Normalno", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 3f62f968c..daebfd579 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -622,6 +622,7 @@ "setup_totp_recommended": "Siapkan TOTP (Disarankan)", "disable_buy": "Nonaktifkan tindakan beli", "disable_sell": "Nonaktifkan aksi jual", + "auto_generate_subaddresses": "Menghasilkan subalamat secara otomatis", "cake_2fa_preset": "Preset Kue 2FA", "narrow": "Sempit", "normal": "Normal", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 8ad7502fd..691abd15c 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -634,6 +634,7 @@ "setup_totp_recommended": "Imposta TOTP (consigliato)", "disable_buy": "Disabilita l'azione di acquisto", "disable_sell": "Disabilita l'azione di vendita", + "auto_generate_subaddresses": "Genera automaticamente sottindirizzi", "cake_2fa_preset": "Torta 2FA Preset", "narrow": "Stretto", "normal": "Normale", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 3fe586279..254d7bbba 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -686,5 +686,6 @@ "support_title_other_links": "その他のサポートリンク", "support_description_other_links": "私たちのコミュニティに参加するか、他の方法を通して私たちのパートナーに連絡してください", "select_destination": "バックアップファイルの保存先を選択してください。", - "save_to_downloads": "ダウンロードに保存" + "save_to_downloads": "ダウンロードに保存", + "auto_generate_subaddresses": "Autoはサブアドレスを生成します" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 1b7116e90..63a446fa3 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -686,5 +686,6 @@ "support_title_other_links": "다른 지원 링크", "support_description_other_links": "다른 방법을 통해 커뮤니티에 가입하거나 파트너에게 연락하십시오.", "select_destination": "백업 파일의 대상을 선택하십시오.", - "save_to_downloads": "다운로드에 저장" + "save_to_downloads": "다운로드에 저장", + "auto_generate_subaddresses": "자동 생성 서브 아드 드레스" } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index c12adb5ac..b131e66d8 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -684,5 +684,6 @@ "support_title_other_links": "အခြားအထောက်အပံ့လင့်များ", "support_description_other_links": "ကျွန်ုပ်တို့၏လူမှုအသိုင်းအဝိုင်းများသို့ 0 င်ရောက်ပါ", "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", - "save_to_downloads": "ဒေါင်းလုဒ်များထံ သိမ်းဆည်းပါ။" + "save_to_downloads": "ဒေါင်းလုဒ်များထံ သိမ်းဆည်းပါ။", + "auto_generate_subaddresses": "အော်တို Generate Subaddresses" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 18b3b5e5b..fcd683fd1 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -634,6 +634,7 @@ "setup_totp_recommended": "TOTP instellen (aanbevolen)", "disable_buy": "Koopactie uitschakelen", "disable_sell": "Verkoopactie uitschakelen", + "auto_generate_subaddresses": "Automatisch subadressen genereren", "cake_2fa_preset": "Taart 2FA Voorinstelling", "narrow": "Smal", "normal": "Normaal", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index e458339ba..17745ccb8 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -634,6 +634,7 @@ "setup_totp_recommended": "Skonfiguruj TOTP (zalecane)", "disable_buy": "Wyłącz akcję kupna", "disable_sell": "Wyłącz akcję sprzedaży", + "auto_generate_subaddresses": "Automatycznie generuj podadresy", "cake_2fa_preset": "Ciasto 2FA Preset", "narrow": "Wąski", "normal": "Normalna", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index aa06cd095..2148e6905 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -633,6 +633,7 @@ "setup_totp_recommended": "Configurar TOTP (recomendado)", "disable_buy": "Desativar ação de compra", "disable_sell": "Desativar ação de venda", + "auto_generate_subaddresses": "Gerar subendereços automaticamente", "cake_2fa_preset": "Predefinição de bolo 2FA", "narrow": "Estreito", "normal": "Normal", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 533ca431d..e229ddee9 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -686,5 +686,6 @@ "support_title_other_links": "Другие ссылки на поддержку", "support_description_other_links": "Присоединяйтесь к нашим сообществам или охватите нас наших партнеров с помощью других методов", "select_destination": "Пожалуйста, выберите место для файла резервной копии.", - "save_to_downloads": "Сохранить в загрузках" + "save_to_downloads": "Сохранить в загрузках", + "auto_generate_subaddresses": "Авто генерируйте Subaddresses" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index f54194617..100929da2 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -684,5 +684,6 @@ "support_title_other_links": "ลิงค์สนับสนุนอื่น ๆ", "support_description_other_links": "เข้าร่วมชุมชนของเราหรือเข้าถึงเราพันธมิตรของเราผ่านวิธีการอื่น ๆ", "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", - "save_to_downloads": "บันทึกลงดาวน์โหลด" + "save_to_downloads": "บันทึกลงดาวน์โหลด", + "auto_generate_subaddresses": "Auto สร้าง subaddresses" } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index fa835912a..e74d1a281 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -632,6 +632,7 @@ "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", + "auto_generate_subaddresses": "Alt adresleri otomatik olarak oluştur", "cake_2fa_preset": "Kek 2FA Ön Ayarı", "narrow": "Dar", "normal": "Normal", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 6d9a648fa..313f6aeb0 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -634,6 +634,7 @@ "setup_totp_recommended": "Налаштувати TOTP (рекомендовано)", "disable_buy": "Вимкнути дію покупки", "disable_sell": "Вимкнути дію продажу", + "auto_generate_subaddresses": "Автоматично генерувати підадреси", "cake_2fa_preset": "Торт 2FA Preset", "narrow": "вузькі", "normal": "нормальний", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 39cae33d0..b8e0c0a00 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -678,5 +678,6 @@ "support_title_other_links": "دوسرے سپورٹ لنکس", "support_description_other_links": "ہماری برادریوں میں شامل ہوں یا دوسرے طریقوں سے ہمارے شراکت داروں تک پہنچیں", "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", - "save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ" + "save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ", + "auto_generate_subaddresses": "آٹو سب ایڈریس تیار کرتا ہے" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 75df8de6b..143fd0ccd 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -680,5 +680,6 @@ "matrix_green_dark_theme": "Matrix Green Dark Akori", "monero_light_theme": "Monero Light Akori", "select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.", - "save_to_downloads": "Fipamọ si Awọn igbasilẹ" + "save_to_downloads": "Fipamọ si Awọn igbasilẹ", + "auto_generate_subaddresses": "Aṣiṣe Ibi-Afọwọkọ" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 3fa31bb10..7e3ceae31 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -685,5 +685,6 @@ "matrix_green_dark_theme": "矩阵绿暗主题", "monero_light_theme": "门罗币浅色主题", "select_destination": "请选择备份文件的目的地。", - "save_to_downloads": "保存到下载" + "save_to_downloads": "保存到下载", + "auto_generate_subaddresses": "自动生成子辅助" } diff --git a/tool/append_translation.dart b/tool/append_translation.dart index e56ad89d6..5c48aceab 100644 --- a/tool/append_translation.dart +++ b/tool/append_translation.dart @@ -1,15 +1,6 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:translator/translator.dart'; - -const defaultLang = "en"; -const langs = [ - "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "id", "it", - "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tr", "uk", "ur", "yo", - "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified) -]; -final translator = GoogleTranslator(); +import 'utils/translation/arb_file_utils.dart'; +import 'utils/translation/translation_constants.dart'; +import 'utils/translation/translation_utils.dart'; void main(List args) async { if (args.length != 2) { @@ -23,44 +14,9 @@ void main(List args) async { print('Appending "$name": "$text"'); for (var lang in langs) { - final fileName = getFileName(lang); + final fileName = getArbFileName(lang); final translation = await getTranslation(text, lang); - appendArbFile(fileName, name, translation); + appendStringToArbFile(fileName, name, translation); } } - -void appendArbFile(String fileName, String name, String text) { - final file = File(fileName); - final inputContent = file.readAsStringSync(); - final arbObj = json.decode(inputContent) as Map; - - if (arbObj.containsKey(name)) { - print("String $name already exists in $fileName!"); - return; - } - - arbObj.addAll({name: text}); - - final outputContent = json - .encode(arbObj) - .replaceAll('","', '",\n "') - .replaceAll('{"', '{\n "') - .replaceAll('"}', '"\n}') - .replaceAll('":"', '": "'); - - file.writeAsStringSync(outputContent); -} - - -Future getTranslation(String text, String lang) async { - if (lang == defaultLang) return text; - return (await translator.translate(text, from: defaultLang, to: lang)).text; -} - -String getFileName(String lang) { - final shortLang = lang - .split("-") - .first; - return "./res/values/strings_$shortLang.arb"; -} diff --git a/tool/translation_consistence.dart b/tool/translation_consistence.dart new file mode 100644 index 000000000..04f64dfc8 --- /dev/null +++ b/tool/translation_consistence.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'utils/translation/arb_file_utils.dart'; +import 'utils/translation/translation_constants.dart'; +import 'utils/translation/translation_utils.dart'; + +void main(List args) async { + print('Checking Consistency of all arb-files. Default: $defaultLang'); + + final doFix = args.contains("--fix"); + + if (doFix) + print('Auto fixing enabled!\n'); + else + print('Auto fixing disabled!\nRun with arg "--fix" to enable autofix\n'); + + final fileName = getArbFileName(defaultLang); + final file = File(fileName); + final arbObj = readArbFile(file); + + for (var lang in langs) { + final fileName = getArbFileName(lang); + final missingKeys = getMissingKeysInArbFile(fileName, arbObj.keys); + if (missingKeys.isNotEmpty) { + final missingDefaults = {}; + + missingKeys.forEach((key) { + print('Missing in "$lang": "$key"'); + if (doFix) + missingDefaults[key] = arbObj[key] as String; + }); + + if (missingDefaults.isNotEmpty) + await appendTranslations(lang, missingDefaults); + } + } +} diff --git a/tool/utils/translation/arb_file_utils.dart b/tool/utils/translation/arb_file_utils.dart new file mode 100644 index 000000000..693c5b93e --- /dev/null +++ b/tool/utils/translation/arb_file_utils.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; +import 'dart:io'; + +void appendStringToArbFile(String fileName, String name, String text) { + final file = File(fileName); + final arbObj = readArbFile(file); + + if (arbObj.containsKey(name)) { + print("String $name already exists in $fileName!"); + return; + } + + arbObj.addAll({name: text}); + + final outputContent = json + .encode(arbObj) + .replaceAll('","', '",\n "') + .replaceAll('{"', '{\n "') + .replaceAll('"}', '"\n}') + .replaceAll('":"', '": "'); + + file.writeAsStringSync(outputContent); +} + +void appendStringsToArbFile(String fileName, Map strings) { + final file = File(fileName); + final arbObj = readArbFile(file); + + arbObj.addAll(strings); + + final outputContent = json + .encode(arbObj) + .replaceAll('","', '",\n "') + .replaceAll('{"', '{\n "') + .replaceAll('"}', '"\n}') + .replaceAll('":"', '": "'); + + file.writeAsStringSync(outputContent); +} + +Map readArbFile(File file) { + final inputContent = file.readAsStringSync(); + + return json.decode(inputContent) as Map; +} + +String getArbFileName(String lang) { + final shortLang = lang + .split("-") + .first; + return "./res/values/strings_$shortLang.arb"; +} + +List getMissingKeysInArbFile(String fileName, Iterable langKeys) { + final file = File(fileName); + final arbObj = readArbFile(file); + final results = []; + + for (var langKey in langKeys) { + if (!arbObj.containsKey(langKey)) { + results.add(langKey); + } + } + + return results; +} diff --git a/tool/utils/translation/translation_constants.dart b/tool/utils/translation/translation_constants.dart new file mode 100644 index 000000000..6563feb32 --- /dev/null +++ b/tool/utils/translation/translation_constants.dart @@ -0,0 +1,6 @@ +const defaultLang = "en"; +const langs = [ + "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "id", "it", + "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tr", "uk", "ur", "yo", + "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified) +]; diff --git a/tool/utils/translation/translation_utils.dart b/tool/utils/translation/translation_utils.dart new file mode 100644 index 000000000..a37838b91 --- /dev/null +++ b/tool/utils/translation/translation_utils.dart @@ -0,0 +1,37 @@ +import 'package:translator/translator.dart'; + +import 'arb_file_utils.dart'; +import 'translation_constants.dart'; + +final translator = GoogleTranslator(); + +Future appendTranslation(String lang, String key, String text) async { + final fileName = getArbFileName(lang); + final translation = await getTranslation(text, lang); + + appendStringToArbFile(fileName, key, translation); +} + +Future appendTranslations(String lang, Map defaults) async { + final fileName = getArbFileName(lang); + final translations = {}; + + for (var key in defaults.keys) { + final value = defaults[key]!; + + if (value.contains("{")) continue; + final translation = await getTranslation(value, lang); + + translations[key] = translation; + } + + print(translations); + + appendStringsToArbFile(fileName, translations); +} + +Future getTranslation(String text, String lang) async { + if (lang == defaultLang) return text; + return (await translator.translate(text, from: defaultLang, to: lang)).text; +} +