CW-228 Auto generate monero subaddress (#902)

* Add UI and setting logic for subaddresses

* Enable auto generate subaddresses

* Rename variable

* Add comment to unused code

* Fix issue with initial state change

* Fix observable for isAppSecure

* Filter sub account contacts

* Fix select account use unused address

* Use add address if last address is unused

* Fix auto generate wallet issues

* Fix button color

* Add translation and refactored naming

* Fix PR review

* Remove unused code

* Remove unused overrides in electrum

* Fix address info null check

* CW-228 Fix ContactListViewModel condition

* CW-228 Fix Account Tile; Rework updateAddressesInBox; Fix _getAllUnusedAddresses

* CW-228 Fix unintentional address_page.dart regression

* CW-228 Fix Merge Conflicts

* CW-228 Add more translation Tools

* CW-228 More merge conflict fixes

* CW-228 Fix Merge Conflicts

* CW-228 Auto Translation improvements

* CW-228 Resolve requested Changes

---------

Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
This commit is contained in:
Godwin Asuquo 2023-08-29 19:11:51 +03:00 committed by GitHub
parent 9999816850
commit fff5a1c419
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 603 additions and 200 deletions

View file

@ -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;
}

View file

@ -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;

View file

@ -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<String, String> addressesMap;
Map<int, List<AddressInfo>> addressInfos;
Set<String> usedAddresses = {};
Future<void> init();
Future<void> 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();

View file

@ -52,6 +52,10 @@ abstract class WalletBase<
late HistoryType transactionHistory;
set isEnabledAutoGenerateSubaddress(bool value) {}
bool get isEnabledAutoGenerateSubaddress => false;
Future<void> connectToNode({required Node node});
Future<void> startSync();

View file

@ -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<int, List<AddressInfo>>? addressInfos;
@HiveField(15)
List<String>? usedAddresses;
String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
set yatLastUsedAddress(String address) {

View file

@ -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<MoneroBalance,
HavenTransactionHistory, HavenTransactionInfo> with Store {
abstract class HavenWalletBase
extends WalletBase<MoneroBalance, HavenTransactionHistory, HavenTransactionInfo> with Store {
HavenWalletBase({required WalletInfo walletInfo})
: balance = ObservableMap.of(getHavenBalance(accountIndex: 0)),
_isTransactionUpdating = false,
@ -47,8 +46,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
syncStatus = NotConnectedSyncStatus(),
super(walletInfo) {
transactionHistory = HavenTransactionHistory();
_onAccountChangeReaction = reaction((_) => walletAddresses.account,
(Account? account) {
_onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) {
if (account == null) {
return;
}
@ -96,14 +94,12 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
haven_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery);
if (haven_wallet.getCurrentHeight() <= 1) {
haven_wallet.setRefreshFromBlockHeight(
height: walletInfo.restoreHeight);
haven_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight);
}
}
_autoSaveTimer = Timer.periodic(
Duration(seconds: _autoSaveInterval),
(_) async => await save());
_autoSaveTimer =
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
}
@override
@ -170,26 +166,25 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
}
if (hasMultiDestination) {
if (outputs.any((item) => 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<MoneroBalance,
final address = output.isParsedAddress && (output.extractedAddress?.isNotEmpty ?? false)
? output.extractedAddress!
: output.address;
final amount = output.sendAll
? null
: output.cryptoAmount!.replaceAll(',', '.');
final int? formattedAmount = output.sendAll
? null
: output.formattedCryptoAmount;
final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.');
final int? formattedAmount = output.sendAll ? null : output.formattedCryptoAmount;
if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
(formattedAmount == null && unlockedBalance <= 0)) {
@ -213,8 +204,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
}
pendingTransactionDescription =
await transaction_history.createTransaction(
pendingTransactionDescription = await transaction_history.createTransaction(
address: address,
assetType: _credentials.assetType,
amount: amount,
@ -307,16 +297,14 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
}
String getTransactionAddress(int accountIndex, int addressIndex) =>
haven_wallet.getAddress(
accountIndex: accountIndex,
addressIndex: addressIndex);
haven_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex);
@override
Future<Map<String, HavenTransactionInfo>> fetchTransactions() async {
haven_transaction_history.refreshTransactions();
return _getAllTransactions(null).fold<Map<String, HavenTransactionInfo>>(
<String, HavenTransactionInfo>{},
(Map<String, HavenTransactionInfo> acc, HavenTransactionInfo tx) {
return _getAllTransactions(null)
.fold<Map<String, HavenTransactionInfo>>(<String, HavenTransactionInfo>{},
(Map<String, HavenTransactionInfo> acc, HavenTransactionInfo tx) {
acc[tx.id] = tx;
return acc;
});
@ -340,9 +328,9 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
}
List<HavenTransactionInfo> _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<MoneroBalance,
}
int _getHeightDistance(DateTime date) {
final distance =
DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch;
final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch;
final daysTmp = (distance / 86400).round();
final days = daysTmp < 1 ? 1 : daysTmp;
@ -386,8 +373,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
void _askForUpdateBalance() =>
balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id));
Future<void> _askForUpdateTransactionHistory() async =>
await updateTransactions();
Future<void> _askForUpdateTransactionHistory() async => await updateTransactions();
void _onNewBlock(int height, int blocksLeft, double ptc) async {
try {
@ -404,9 +390,9 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
syncStatus = SyncedSyncStatus();
if (!_hasSyncAfterStartup) {
_hasSyncAfterStartup = true;
await save();
}
_hasSyncAfterStartup = true;
await save();
}
if (walletInfo.isRecovery) {
await setAsRecovered();

View file

@ -6,14 +6,15 @@ import 'package:cw_core/subaddress.dart';
part 'monero_subaddress_list.g.dart';
class MoneroSubaddressList = MoneroSubaddressListBase
with _$MoneroSubaddressList;
class MoneroSubaddressList = MoneroSubaddressListBase with _$MoneroSubaddressList;
abstract class MoneroSubaddressListBase with Store {
MoneroSubaddressListBase()
: _isRefreshing = false,
_isUpdating = false,
subaddresses = ObservableList<Subaddress>();
: _isRefreshing = false,
_isUpdating = false,
subaddresses = ObservableList<Subaddress>();
final List<String> _usedAddresses = [];
@observable
ObservableList<Subaddress> 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<void> 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<void> updateWithAutoGenerate({
required int accountIndex,
required String defaultLabel,
required List<String> 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<List<Subaddress>> _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<bool> _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;
}
}

View file

@ -48,12 +48,14 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}),
_isTransactionUpdating = false,
_hasSyncAfterStartup = false,
walletAddresses = MoneroWalletAddresses(walletInfo),
isEnabledAutoGenerateSubaddress = false,
syncStatus = NotConnectedSyncStatus(),
unspentCoins = [],
this.unspentCoinsInfo = unspentCoinsInfo,
super(walletInfo) {
transactionHistory = MoneroTransactionHistory();
walletAddresses = MoneroWalletAddresses(walletInfo, transactionHistory);
_onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) {
if (account == null) {
return;
@ -64,7 +66,11 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
fullBalance: monero_wallet.getFullBalance(accountIndex: account.id),
unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id))
});
walletAddresses.updateSubaddressList(accountIndex: account.id);
_updateSubAddress(isEnabledAutoGenerateSubaddress, account: account);
});
reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
_updateSubAddress(enabled, account: walletAddresses.account);
});
}
@ -73,7 +79,11 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
Box<UnspentCoinsInfo> unspentCoinsInfo;
@override
MoneroWalletAddresses walletAddresses;
late MoneroWalletAddresses walletAddresses;
@override
@observable
bool isEnabledAutoGenerateSubaddress;
@override
@observable
@ -287,6 +297,14 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
@override
Future<void> 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<MoneroBalance,
print(e.toString());
}
}
void _updateSubAddress(bool enableAutoGenerate, {Account? account}) {
if (enableAutoGenerate) {
walletAddresses.updateUnusedSubaddress(
accountIndex: account?.id ?? 0,
defaultLabel: account?.label ?? '',
);
} else {
walletAddresses.updateSubaddressList(accountIndex: account?.id ?? 0);
}
}
}

View file

@ -1,23 +1,28 @@
import 'package:cw_core/address_info.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/account.dart';
import 'package:cw_monero/api/wallet.dart';
import 'package:cw_monero/monero_account_list.dart';
import 'package:cw_monero/monero_subaddress_list.dart';
import 'package:cw_core/subaddress.dart';
import 'package:cw_monero/monero_transaction_history.dart';
import 'package:mobx/mobx.dart';
part 'monero_wallet_addresses.g.dart';
class MoneroWalletAddresses = MoneroWalletAddressesBase
with _$MoneroWalletAddresses;
class MoneroWalletAddresses = MoneroWalletAddressesBase with _$MoneroWalletAddresses;
abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
MoneroWalletAddressesBase(WalletInfo walletInfo)
: accountList = MoneroAccountList(),
subaddressList = MoneroSubaddressList(),
address = '',
super(walletInfo);
MoneroWalletAddressesBase(
WalletInfo walletInfo, MoneroTransactionHistory moneroTransactionHistory)
: accountList = MoneroAccountList(),
_moneroTransactionHistory = moneroTransactionHistory,
subaddressList = MoneroSubaddressList(),
address = '',
super(walletInfo);
final MoneroTransactionHistory _moneroTransactionHistory;
@override
@observable
String address;
@ -36,7 +41,6 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
Future<void> 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;
}
Future<void> 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<void> 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;
}
}

View file

@ -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);

View file

@ -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<void> setup({
if (!_isSetupFinished) {
getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance());
}
if (!_isSetupFinished) {
getIt.registerFactory(() => BackgroundTasks());
}

View file

@ -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);
}

View file

@ -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';

View file

@ -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<void> initializeAppConfigs() async {
CakeHive.registerAdapter(TradeAdapter());
}
if (!CakeHive.isAdapterRegistered(AddressInfo.typeId)) {
CakeHive.registerAdapter(AddressInfoAdapter());
}
if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) {
CakeHive.registerAdapter(WalletInfoAdapter());
}

View file

@ -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<YatStore>();
// 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<YatStore>();
// 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>, TransactionInfo>?
wallet) async {
_onCurrentWalletChangeReaction = reaction((_) => appStore.wallet,
(WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, 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<SharedPreferences>().setString(PreferencesKey.currentWalletName, wallet.name);
await getIt
.get<SharedPreferences>()
.setString(PreferencesKey.currentWalletName, wallet.name);
await getIt.get<SharedPreferences>().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<Balance,
TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
_onCurrentWalletChangeFiatRateUpdateReaction = reaction((_) => appStore.wallet,
(WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, 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<Balance, TransactionHistoryBase<TransactionInfo>, 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;
}

View file

@ -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<void>(
context: context,
builder: (_) => getIt.get<MoneroAccountListPage>())
: 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: <Widget>[
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<SyncIndicatorTheme>()!
.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(

View file

@ -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;

View file

@ -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());

View file

@ -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,

View file

@ -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 = <WalletType, Node>{};
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;

View file

@ -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<ContactRecord>(),
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<Contact> contactSource;
final Box<WalletInfo> walletInfoSource;
final ObservableList<ContactRecord> contacts;

View file

@ -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<ActionListItem> get items {
final _items = <ActionListItem>[];

View file

@ -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<Balance, TransactionHistoryBase<TransactionInfo>, 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;

View file

@ -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"
}

View file

@ -626,6 +626,7 @@
"setup_totp_recommended": "Настройка на TOTP (препоръчително)",
"disable_buy": "Деактивирайте действието за покупка",
"disable_sell": "Деактивирайте действието за продажба",
"auto_generate_subaddresses": "Автоматично генериране на подадреси",
"cake_2fa_preset": "Торта 2FA Preset",
"narrow": "Тесен",
"normal": "нормално",

View file

@ -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í",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -640,6 +640,7 @@
"high_contrast_theme": "उच्च कंट्रास्ट थीम",
"matrix_green_dark_theme": "मैट्रिक्स ग्रीन डार्क थीम",
"monero_light_theme": "मोनेरो लाइट थीम",
"auto_generate_subaddresses": "स्वचालित रूप से उप-पते उत्पन्न करें",
"cake_2fa_preset": "केक 2एफए प्रीसेट",
"narrow": "सँकरा",
"normal": "सामान्य",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -686,5 +686,6 @@
"support_title_other_links": "その他のサポートリンク",
"support_description_other_links": "私たちのコミュニティに参加するか、他の方法を通して私たちのパートナーに連絡してください",
"select_destination": "バックアップファイルの保存先を選択してください。",
"save_to_downloads": "ダウンロードに保存"
"save_to_downloads": "ダウンロードに保存",
"auto_generate_subaddresses": "Autoはサブアドレスを生成します"
}

View file

@ -686,5 +686,6 @@
"support_title_other_links": "다른 지원 링크",
"support_description_other_links": "다른 방법을 통해 커뮤니티에 가입하거나 파트너에게 연락하십시오.",
"select_destination": "백업 파일의 대상을 선택하십시오.",
"save_to_downloads": "다운로드에 저장"
"save_to_downloads": "다운로드에 저장",
"auto_generate_subaddresses": "자동 생성 서브 아드 드레스"
}

View file

@ -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"
}

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -686,5 +686,6 @@
"support_title_other_links": "Другие ссылки на поддержку",
"support_description_other_links": "Присоединяйтесь к нашим сообществам или охватите нас наших партнеров с помощью других методов",
"select_destination": "Пожалуйста, выберите место для файла резервной копии.",
"save_to_downloads": "Сохранить в загрузках"
"save_to_downloads": "Сохранить в загрузках",
"auto_generate_subaddresses": "Авто генерируйте Subaddresses"
}

View file

@ -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"
}

View file

@ -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",

View file

@ -634,6 +634,7 @@
"setup_totp_recommended": "Налаштувати TOTP (рекомендовано)",
"disable_buy": "Вимкнути дію покупки",
"disable_sell": "Вимкнути дію продажу",
"auto_generate_subaddresses": "Автоматично генерувати підадреси",
"cake_2fa_preset": "Торт 2FA Preset",
"narrow": "вузькі",
"normal": "нормальний",

View file

@ -678,5 +678,6 @@
"support_title_other_links": "دوسرے سپورٹ لنکس",
"support_description_other_links": "ہماری برادریوں میں شامل ہوں یا دوسرے طریقوں سے ہمارے شراکت داروں تک پہنچیں",
"select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ",
"save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ"
"save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ",
"auto_generate_subaddresses": "آٹو سب ایڈریس تیار کرتا ہے"
}

View file

@ -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ọ"
}

View file

@ -685,5 +685,6 @@
"matrix_green_dark_theme": "矩阵绿暗主题",
"monero_light_theme": "门罗币浅色主题",
"select_destination": "请选择备份文件的目的地。",
"save_to_downloads": "保存到下载"
"save_to_downloads": "保存到下载",
"auto_generate_subaddresses": "自动生成子辅助"
}

View file

@ -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<String> args) async {
if (args.length != 2) {
@ -23,44 +14,9 @@ void main(List<String> 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<String, dynamic>;
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<String> 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";
}

View file

@ -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<String> 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 = <String, String>{};
missingKeys.forEach((key) {
print('Missing in "$lang": "$key"');
if (doFix)
missingDefaults[key] = arbObj[key] as String;
});
if (missingDefaults.isNotEmpty)
await appendTranslations(lang, missingDefaults);
}
}
}

View file

@ -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<String, String> 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<String, dynamic> readArbFile(File file) {
final inputContent = file.readAsStringSync();
return json.decode(inputContent) as Map<String, dynamic>;
}
String getArbFileName(String lang) {
final shortLang = lang
.split("-")
.first;
return "./res/values/strings_$shortLang.arb";
}
List<String> getMissingKeysInArbFile(String fileName, Iterable<String> langKeys) {
final file = File(fileName);
final arbObj = readArbFile(file);
final results = <String>[];
for (var langKey in langKeys) {
if (!arbObj.containsKey(langKey)) {
results.add(langKey);
}
}
return results;
}

View file

@ -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)
];

View file

@ -0,0 +1,37 @@
import 'package:translator/translator.dart';
import 'arb_file_utils.dart';
import 'translation_constants.dart';
final translator = GoogleTranslator();
Future<void> appendTranslation(String lang, String key, String text) async {
final fileName = getArbFileName(lang);
final translation = await getTranslation(text, lang);
appendStringToArbFile(fileName, key, translation);
}
Future<void> appendTranslations(String lang, Map<String, String> defaults) async {
final fileName = getArbFileName(lang);
final translations = <String, String>{};
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<String> getTranslation(String text, String lang) async {
if (lang == defaultLang) return text;
return (await translator.translate(text, from: defaultLang, to: lang)).text;
}