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 ORDER_TYPE_ID = 8;
const UNSPENT_COINS_INFO_TYPE_ID = 9; const UNSPENT_COINS_INFO_TYPE_ID = 9;
const ANONPAY_INVOICE_INFO_TYPE_ID = 10; const ANONPAY_INVOICE_INFO_TYPE_ID = 10;
const ADDRESS_INFO_TYPE_ID = 11;
const ERC20_TOKEN_TYPE_ID = 12; 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'; import 'package:cw_core/wallet_info.dart';
abstract class WalletAddresses { abstract class WalletAddresses {
WalletAddresses(this.walletInfo) WalletAddresses(this.walletInfo)
: addressesMap = {}; : addressesMap = {},
addressInfos = {};
final WalletInfo walletInfo; final WalletInfo walletInfo;
@ -12,6 +14,10 @@ abstract class WalletAddresses {
Map<String, String> addressesMap; Map<String, String> addressesMap;
Map<int, List<AddressInfo>> addressInfos;
Set<String> usedAddresses = {};
Future<void> init(); Future<void> init();
Future<void> updateAddressesInBox(); Future<void> updateAddressesInBox();
@ -20,6 +26,8 @@ abstract class WalletAddresses {
try { try {
walletInfo.address = address; walletInfo.address = address;
walletInfo.addresses = addressesMap; walletInfo.addresses = addressesMap;
walletInfo.addressInfos = addressInfos;
walletInfo.usedAddresses = usedAddresses.toList();
if (walletInfo.isInBox) { if (walletInfo.isInBox) {
await walletInfo.save(); await walletInfo.save();

View file

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

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:cw_core/address_info.dart';
import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -72,6 +73,12 @@ class WalletInfo extends HiveObject {
@HiveField(13) @HiveField(13)
bool? showIntroCakePayCard; bool? showIntroCakePayCard;
@HiveField(14)
Map<int, List<AddressInfo>>? addressInfos;
@HiveField(15)
List<String>? usedAddresses;
String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
set yatLastUsedAddress(String address) { 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:cw_haven/api/structs/pending_transaction.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cw_haven/api/transaction_history.dart' import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history;
as haven_transaction_history;
//import 'package:cw_haven/wallet.dart'; //import 'package:cw_haven/wallet.dart';
import 'package:cw_haven/api/wallet.dart' as haven_wallet; import 'package:cw_haven/api/wallet.dart' as haven_wallet;
import 'package:cw_haven/api/transaction_history.dart' as transaction_history; import 'package:cw_haven/api/transaction_history.dart' as transaction_history;
@ -37,8 +36,8 @@ const moneroBlockSize = 1000;
class HavenWallet = HavenWalletBase with _$HavenWallet; class HavenWallet = HavenWalletBase with _$HavenWallet;
abstract class HavenWalletBase extends WalletBase<MoneroBalance, abstract class HavenWalletBase
HavenTransactionHistory, HavenTransactionInfo> with Store { extends WalletBase<MoneroBalance, HavenTransactionHistory, HavenTransactionInfo> with Store {
HavenWalletBase({required WalletInfo walletInfo}) HavenWalletBase({required WalletInfo walletInfo})
: balance = ObservableMap.of(getHavenBalance(accountIndex: 0)), : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)),
_isTransactionUpdating = false, _isTransactionUpdating = false,
@ -47,8 +46,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
syncStatus = NotConnectedSyncStatus(), syncStatus = NotConnectedSyncStatus(),
super(walletInfo) { super(walletInfo) {
transactionHistory = HavenTransactionHistory(); transactionHistory = HavenTransactionHistory();
_onAccountChangeReaction = reaction((_) => walletAddresses.account, _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) {
(Account? account) {
if (account == null) { if (account == null) {
return; return;
} }
@ -96,14 +94,12 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
haven_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery); haven_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery);
if (haven_wallet.getCurrentHeight() <= 1) { if (haven_wallet.getCurrentHeight() <= 1) {
haven_wallet.setRefreshFromBlockHeight( haven_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight);
height: walletInfo.restoreHeight);
} }
} }
_autoSaveTimer = Timer.periodic( _autoSaveTimer =
Duration(seconds: _autoSaveInterval), Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
(_) async => await save());
} }
@override @override
@ -115,7 +111,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
_onAccountChangeReaction?.reaction.dispose(); _onAccountChangeReaction?.reaction.dispose();
_autoSaveTimer?.cancel(); _autoSaveTimer?.cancel();
} }
@override @override
Future<void> connectToNode({required Node node}) async { Future<void> connectToNode({required Node node}) async {
try { try {
@ -170,26 +166,25 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
} }
if (hasMultiDestination) { if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|| (item.formattedCryptoAmount ?? 0) <= 0)) { throw HavenTransactionCreationException(
throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); 'You do not have enough coins to send this amount.');
} }
final int totalAmount = outputs.fold(0, (acc, value) => final int totalAmount =
acc + (value.formattedCryptoAmount ?? 0)); outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
if (unlockedBalance < totalAmount) { 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) => final moneroOutputs = outputs
MoneroOutput( .map((output) => MoneroOutput(
address: output.address, address: output.address, amount: output.cryptoAmount!.replaceAll(',', '.')))
amount: output.cryptoAmount!.replaceAll(',', '.')))
.toList(); .toList();
pendingTransactionDescription = pendingTransactionDescription = await transaction_history.createTransactionMultDest(
await transaction_history.createTransactionMultDest(
outputs: moneroOutputs, outputs: moneroOutputs,
priorityRaw: _credentials.priority.serialize(), priorityRaw: _credentials.priority.serialize(),
accountIndex: walletAddresses.account!.id); accountIndex: walletAddresses.account!.id);
@ -198,12 +193,8 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
final address = output.isParsedAddress && (output.extractedAddress?.isNotEmpty ?? false) final address = output.isParsedAddress && (output.extractedAddress?.isNotEmpty ?? false)
? output.extractedAddress! ? output.extractedAddress!
: output.address; : output.address;
final amount = output.sendAll final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.');
? null final int? formattedAmount = output.sendAll ? null : output.formattedCryptoAmount;
: output.cryptoAmount!.replaceAll(',', '.');
final int? formattedAmount = output.sendAll
? null
: output.formattedCryptoAmount;
if ((formattedAmount != null && unlockedBalance < formattedAmount) || if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
(formattedAmount == null && unlockedBalance <= 0)) { (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}.'); 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
} }
pendingTransactionDescription = pendingTransactionDescription = await transaction_history.createTransaction(
await transaction_history.createTransaction(
address: address, address: address,
assetType: _credentials.assetType, assetType: _credentials.assetType,
amount: amount, amount: amount,
@ -307,16 +297,14 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
} }
String getTransactionAddress(int accountIndex, int addressIndex) => String getTransactionAddress(int accountIndex, int addressIndex) =>
haven_wallet.getAddress( haven_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex);
accountIndex: accountIndex,
addressIndex: addressIndex);
@override @override
Future<Map<String, HavenTransactionInfo>> fetchTransactions() async { Future<Map<String, HavenTransactionInfo>> fetchTransactions() async {
haven_transaction_history.refreshTransactions(); haven_transaction_history.refreshTransactions();
return _getAllTransactions(null).fold<Map<String, HavenTransactionInfo>>( return _getAllTransactions(null)
<String, HavenTransactionInfo>{}, .fold<Map<String, HavenTransactionInfo>>(<String, HavenTransactionInfo>{},
(Map<String, HavenTransactionInfo> acc, HavenTransactionInfo tx) { (Map<String, HavenTransactionInfo> acc, HavenTransactionInfo tx) {
acc[tx.id] = tx; acc[tx.id] = tx;
return acc; return acc;
}); });
@ -340,9 +328,9 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
} }
List<HavenTransactionInfo> _getAllTransactions(dynamic _) => haven_transaction_history List<HavenTransactionInfo> _getAllTransactions(dynamic _) => haven_transaction_history
.getAllTransations() .getAllTransations()
.map((row) => HavenTransactionInfo.fromRow(row)) .map((row) => HavenTransactionInfo.fromRow(row))
.toList(); .toList();
void _setListeners() { void _setListeners() {
_listener?.stop(); _listener?.stop();
@ -364,8 +352,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
} }
int _getHeightDistance(DateTime date) { int _getHeightDistance(DateTime date) {
final distance = final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch;
DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch;
final daysTmp = (distance / 86400).round(); final daysTmp = (distance / 86400).round();
final days = daysTmp < 1 ? 1 : daysTmp; final days = daysTmp < 1 ? 1 : daysTmp;
@ -386,8 +373,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
void _askForUpdateBalance() => void _askForUpdateBalance() =>
balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id)); balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id));
Future<void> _askForUpdateTransactionHistory() async => Future<void> _askForUpdateTransactionHistory() async => await updateTransactions();
await updateTransactions();
void _onNewBlock(int height, int blocksLeft, double ptc) async { void _onNewBlock(int height, int blocksLeft, double ptc) async {
try { try {
@ -404,9 +390,9 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
if (!_hasSyncAfterStartup) { if (!_hasSyncAfterStartup) {
_hasSyncAfterStartup = true; _hasSyncAfterStartup = true;
await save(); await save();
} }
if (walletInfo.isRecovery) { if (walletInfo.isRecovery) {
await setAsRecovered(); await setAsRecovered();

View file

@ -6,14 +6,15 @@ import 'package:cw_core/subaddress.dart';
part 'monero_subaddress_list.g.dart'; part 'monero_subaddress_list.g.dart';
class MoneroSubaddressList = MoneroSubaddressListBase class MoneroSubaddressList = MoneroSubaddressListBase with _$MoneroSubaddressList;
with _$MoneroSubaddressList;
abstract class MoneroSubaddressListBase with Store { abstract class MoneroSubaddressListBase with Store {
MoneroSubaddressListBase() MoneroSubaddressListBase()
: _isRefreshing = false, : _isRefreshing = false,
_isUpdating = false, _isUpdating = false,
subaddresses = ObservableList<Subaddress>(); subaddresses = ObservableList<Subaddress>();
final List<String> _usedAddresses = [];
@observable @observable
ObservableList<Subaddress> subaddresses; ObservableList<Subaddress> subaddresses;
@ -49,20 +50,24 @@ abstract class MoneroSubaddressListBase with Store {
subaddresses = [primary] + rest.toList(); subaddresses = [primary] + rest.toList();
} }
return subaddresses return subaddresses.map((subaddressRow) {
.map((subaddressRow) => Subaddress( 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(), id: subaddressRow.getId(),
address: subaddressRow.getAddress(), address: subaddressRow.getAddress(),
label: subaddressRow.getId() == 0 && label: isPrimaryAddress
subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address'
? 'Primary address' : hasDefaultAddressName
: subaddressRow.getLabel())) ? ''
.toList(); : subaddressRow.getLabel());
}).toList();
} }
Future<void> addSubaddress({required int accountIndex, required String label}) async { Future<void> addSubaddress({required int accountIndex, required String label}) async {
await subaddress_list.addSubaddress( await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label);
accountIndex: accountIndex, label: label);
update(accountIndex: accountIndex); update(accountIndex: accountIndex);
} }
@ -88,4 +93,59 @@ abstract class MoneroSubaddressListBase with Store {
rethrow; 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, _isTransactionUpdating = false,
_hasSyncAfterStartup = false, _hasSyncAfterStartup = false,
walletAddresses = MoneroWalletAddresses(walletInfo), isEnabledAutoGenerateSubaddress = false,
syncStatus = NotConnectedSyncStatus(), syncStatus = NotConnectedSyncStatus(),
unspentCoins = [], unspentCoins = [],
this.unspentCoinsInfo = unspentCoinsInfo, this.unspentCoinsInfo = unspentCoinsInfo,
super(walletInfo) { super(walletInfo) {
transactionHistory = MoneroTransactionHistory(); transactionHistory = MoneroTransactionHistory();
walletAddresses = MoneroWalletAddresses(walletInfo, transactionHistory);
_onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) {
if (account == null) { if (account == null) {
return; return;
@ -64,7 +66,11 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), fullBalance: monero_wallet.getFullBalance(accountIndex: account.id),
unlockedBalance: monero_wallet.getUnlockedBalance(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; Box<UnspentCoinsInfo> unspentCoinsInfo;
@override @override
MoneroWalletAddresses walletAddresses; late MoneroWalletAddresses walletAddresses;
@override
@observable
bool isEnabledAutoGenerateSubaddress;
@override @override
@observable @observable
@ -287,6 +297,14 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
@override @override
Future<void> save() async { Future<void> save() async {
await walletAddresses.updateUsedSubaddress();
if (isEnabledAutoGenerateSubaddress) {
walletAddresses.updateUnusedSubaddress(
accountIndex: walletAddresses.account?.id ?? 0,
defaultLabel: walletAddresses.account?.label ?? '');
}
await walletAddresses.updateAddressesInBox(); await walletAddresses.updateAddressesInBox();
await backupWalletFiles(name); await backupWalletFiles(name);
await monero_wallet.store(); await monero_wallet.store();
@ -610,4 +628,15 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
print(e.toString()); 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,27 +1,32 @@
import 'package:cw_core/address_info.dart';
import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/account.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_account_list.dart';
import 'package:cw_monero/monero_subaddress_list.dart'; import 'package:cw_monero/monero_subaddress_list.dart';
import 'package:cw_core/subaddress.dart'; import 'package:cw_core/subaddress.dart';
import 'package:cw_monero/monero_transaction_history.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
part 'monero_wallet_addresses.g.dart'; part 'monero_wallet_addresses.g.dart';
class MoneroWalletAddresses = MoneroWalletAddressesBase class MoneroWalletAddresses = MoneroWalletAddressesBase with _$MoneroWalletAddresses;
with _$MoneroWalletAddresses;
abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
MoneroWalletAddressesBase(WalletInfo walletInfo) MoneroWalletAddressesBase(
: accountList = MoneroAccountList(), WalletInfo walletInfo, MoneroTransactionHistory moneroTransactionHistory)
subaddressList = MoneroSubaddressList(), : accountList = MoneroAccountList(),
address = '', _moneroTransactionHistory = moneroTransactionHistory,
super(walletInfo); subaddressList = MoneroSubaddressList(),
address = '',
super(walletInfo);
final MoneroTransactionHistory _moneroTransactionHistory;
@override @override
@observable @observable
String address; String address;
@observable @observable
Account? account; Account? account;
@ -36,7 +41,6 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
Future<void> init() async { Future<void> init() async {
accountList.update(); accountList.update();
account = accountList.accounts.first; account = accountList.accounts.first;
updateSubaddressList(accountIndex: account?.id ?? 0);
await updateAddressesInBox(); await updateAddressesInBox();
} }
@ -46,11 +50,15 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
final _subaddressList = MoneroSubaddressList(); final _subaddressList = MoneroSubaddressList();
addressesMap.clear(); addressesMap.clear();
addressInfos.clear();
accountList.accounts.forEach((account) { accountList.accounts.forEach((account) {
_subaddressList.update(accountIndex: account.id); _subaddressList.update(accountIndex: account.id);
_subaddressList.subaddresses.forEach((subaddress) { _subaddressList.subaddresses.forEach((subaddress) {
addressesMap[subaddress.address] = subaddress.label; 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() { bool validate() {
accountList.update(); accountList.update();
final accountListLength = accountList.accounts.length ?? 0; final accountListLength = accountList.accounts.length;
if (accountListLength <= 0) { if (accountListLength <= 0) {
return false; return false;
} }
subaddressList.update(accountIndex: accountList.accounts.first.id); subaddressList.update(accountIndex: accountList.accounts.first.id);
final subaddressListLength = subaddressList.subaddresses.length ?? 0; final subaddressListLength = subaddressList.subaddresses.length;
if (subaddressListLength <= 0) { if (subaddressListLength <= 0) {
return false; return false;
@ -83,4 +91,24 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
subaddress = subaddressList.subaddresses.first; subaddress = subaddressList.subaddresses.first;
address = subaddress!.address; 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 useEtherscan = data[PreferencesKey.useEtherscan] as bool?;
final syncAll = data[PreferencesKey.syncAllKey] as bool?; final syncAll = data[PreferencesKey.syncAllKey] as bool?;
final syncMode = data[PreferencesKey.syncModeKey] as int?; final syncMode = data[PreferencesKey.syncModeKey] as int?;
final autoGenerateSubaddressStatus = data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?;
await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
@ -296,6 +297,9 @@ class BackupService {
if (fiatApiMode != null) if (fiatApiMode != null)
await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode);
if (autoGenerateSubaddressStatus != null)
await _sharedPreferences.setInt(PreferencesKey.autoGenerateSubaddressStatusKey,
autoGenerateSubaddressStatus);
if (currentPinLength != null) if (currentPinLength != null)
await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength);
@ -523,6 +527,8 @@ class BackupService {
_sharedPreferences.getInt(PreferencesKey.syncModeKey), _sharedPreferences.getInt(PreferencesKey.syncModeKey),
PreferencesKey.syncAllKey: PreferencesKey.syncAllKey:
_sharedPreferences.getBool(PreferencesKey.syncAllKey), _sharedPreferences.getBool(PreferencesKey.syncAllKey),
PreferencesKey.autoGenerateSubaddressStatusKey:
_sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey),
}; };
return json.encode(preferences); 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/buy/payfura/payfura_buy_provider.dart';
import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/background_tasks.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/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/receive_page_option.dart'; import 'package:cake_wallet/entities/receive_page_option.dart';
@ -245,7 +246,6 @@ Future<void> setup({
if (!_isSetupFinished) { if (!_isSetupFinished) {
getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance()); getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance());
} }
if (!_isSetupFinished) { if (!_isSetupFinished) {
getIt.registerFactory(() => BackgroundTasks()); 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}'; '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
static const exchangeProvidersSelection = 'exchange-providers-selection'; static const exchangeProvidersSelection = 'exchange-providers-selection';
static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status';
static const clearnetDonationLink = 'clearnet_donation_link'; static const clearnetDonationLink = 'clearnet_donation_link';
static const onionDonationLink = 'onion_donation_link'; static const onionDonationLink = 'onion_donation_link';
static const lastSeenAppVersion = 'last_seen_app_version'; 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/locales/locale.dart';
import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cake_wallet/utils/exception_handler.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:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/hive_type_ids.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -89,6 +90,10 @@ Future<void> initializeAppConfigs() async {
CakeHive.registerAdapter(TradeAdapter()); CakeHive.registerAdapter(TradeAdapter());
} }
if (!CakeHive.isAdapterRegistered(AddressInfo.typeId)) {
CakeHive.registerAdapter(AddressInfoAdapter());
}
if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) { if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) {
CakeHive.registerAdapter(WalletInfoAdapter()); 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/fiat_api_mode.dart';
import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
@ -21,36 +22,36 @@ ReactionDisposer? _onCurrentWalletChangeReaction;
ReactionDisposer? _onCurrentWalletChangeFiatRateUpdateReaction; ReactionDisposer? _onCurrentWalletChangeFiatRateUpdateReaction;
//ReactionDisposer _onCurrentWalletAddressChangeReaction; //ReactionDisposer _onCurrentWalletAddressChangeReaction;
void startCurrentWalletChangeReaction(AppStore appStore, void startCurrentWalletChangeReaction(
SettingsStore settingsStore, FiatConversionStore fiatConversionStore) { AppStore appStore, SettingsStore settingsStore, FiatConversionStore fiatConversionStore) {
_onCurrentWalletChangeReaction?.reaction.dispose(); _onCurrentWalletChangeReaction?.reaction.dispose();
_onCurrentWalletChangeFiatRateUpdateReaction?.reaction.dispose(); _onCurrentWalletChangeFiatRateUpdateReaction?.reaction.dispose();
//_onCurrentWalletAddressChangeReaction?.reaction?dispose(); //_onCurrentWalletAddressChangeReaction?.reaction?dispose();
//_onCurrentWalletAddressChangeReaction = reaction((_) => appStore.wallet.walletAddresses.address, //_onCurrentWalletAddressChangeReaction = reaction((_) => appStore.wallet.walletAddresses.address,
//(String address) async { //(String address) async {
//if (address == appStore.wallet.walletInfo.yatLastUsedAddress) { //if (address == appStore.wallet.walletInfo.yatLastUsedAddress) {
// return; // return;
//} //}
//try { //try {
// final yatStore = getIt.get<YatStore>(); // final yatStore = getIt.get<YatStore>();
// await updateEmojiIdAddress( // await updateEmojiIdAddress(
// appStore.wallet.walletInfo.yatEmojiId, // appStore.wallet.walletInfo.yatEmojiId,
// appStore.wallet.walletAddresses.address, // appStore.wallet.walletAddresses.address,
// yatStore.apiKey, // yatStore.apiKey,
// appStore.wallet.type // appStore.wallet.type
// ); // );
// appStore.wallet.walletInfo.yatLastUsedAddress = address; // appStore.wallet.walletInfo.yatLastUsedAddress = address;
// await appStore.wallet.walletInfo.save(); // await appStore.wallet.walletInfo.save();
//} catch (e) { //} catch (e) {
// print(e.toString()); // print(e.toString());
//} //}
//}); //});
_onCurrentWalletChangeReaction = reaction((_) => appStore.wallet, (WalletBase< _onCurrentWalletChangeReaction = reaction((_) => appStore.wallet,
Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? (WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
wallet) async { wallet) async {
try { try {
if (wallet == null) { if (wallet == null) {
return; return;
@ -59,11 +60,13 @@ void startCurrentWalletChangeReaction(AppStore appStore,
final node = settingsStore.getCurrentNode(wallet.type); final node = settingsStore.getCurrentNode(wallet.type);
startWalletSyncStatusChangeReaction(wallet, fiatConversionStore); startWalletSyncStatusChangeReaction(wallet, fiatConversionStore);
startCheckConnectionReaction(wallet, settingsStore); startCheckConnectionReaction(wallet, settingsStore);
await getIt.get<SharedPreferences>().setString(PreferencesKey.currentWalletName, wallet.name);
await getIt await getIt
.get<SharedPreferences>() .get<SharedPreferences>()
.setString(PreferencesKey.currentWalletName, wallet.name); .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type));
await getIt.get<SharedPreferences>().setInt( if (wallet.type == WalletType.monero) {
PreferencesKey.currentWalletType, serializeToInt(wallet.type)); _setAutoGenerateSubaddressStatus(wallet, settingsStore);
}
await wallet.connectToNode(node: node); await wallet.connectToNode(node: node);
if (wallet.type == WalletType.haven) { if (wallet.type == WalletType.haven) {
@ -82,9 +85,8 @@ void startCurrentWalletChangeReaction(AppStore appStore,
} }
}); });
_onCurrentWalletChangeFiatRateUpdateReaction = _onCurrentWalletChangeFiatRateUpdateReaction = reaction((_) => appStore.wallet,
reaction((_) => appStore.wallet, (WalletBase<Balance, (WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
wallet) async { wallet) async {
try { try {
if (wallet == null || settingsStore.fiatApiMode == FiatApiMode.disabled) { if (wallet == null || settingsStore.fiatApiMode == FiatApiMode.disabled) {
@ -92,11 +94,10 @@ void startCurrentWalletChangeReaction(AppStore appStore,
} }
fiatConversionStore.prices[wallet.currency] = 0; fiatConversionStore.prices[wallet.currency] = 0;
fiatConversionStore.prices[wallet.currency] = fiatConversionStore.prices[wallet.currency] = await FiatConversionService.fetchPrice(
await FiatConversionService.fetchPrice( crypto: wallet.currency,
crypto: wallet.currency, fiat: settingsStore.fiatCurrency,
fiat: settingsStore.fiatCurrency, torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
if (wallet.type == WalletType.ethereum) { if (wallet.type == WalletType.ethereum) {
final currencies = 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/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/anonpay/anonpay_donation_link_info.dart';
import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/receive_page_option.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/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/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/gradient_background.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:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.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/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
@ -174,7 +175,11 @@ class AddressPage extends BasePage {
Observer(builder: (_) { Observer(builder: (_) {
if (addressListViewModel.hasAddressList) { if (addressListViewModel.hasAddressList) {
return GestureDetector( 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( child: Container(
height: 50, height: 50,
padding: EdgeInsets.only(left: 24, right: 12), padding: EdgeInsets.only(left: 24, right: 12),
@ -193,17 +198,26 @@ class AddressPage extends BasePage {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Observer( Observer(
builder: (_) => Text( builder: (_) {
addressListViewModel.hasAccounts String label = addressListViewModel.hasAccounts
? S.of(context).accounts_subaddresses ? S.of(context).accounts_subaddresses
: S.of(context).addresses, : S.of(context).addresses;
style: TextStyle(
fontSize: 14, if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) {
fontWeight: FontWeight.w500, label = addressListViewModel.hasAccounts
color: Theme.of(context) ? S.of(context).accounts
: S.of(context).account;
}
return Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<SyncIndicatorTheme>()! .extension<SyncIndicatorTheme>()!
.textColor), .textColor),
)), );
},),
Icon( Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 14, 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, return Text(S.of(context).electrum_address_disclaimer,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(

View file

@ -5,13 +5,14 @@ import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
class AccountTile extends StatelessWidget { class AccountTile extends StatelessWidget {
AccountTile( AccountTile({
{required this.isCurrent, required this.isCurrent,
required this.accountName, required this.accountName,
this.accountBalance, this.accountBalance,
required this.currency, required this.currency,
required this.onTap, required this.onTap,
required this.onEdit}); required this.onEdit,
});
final bool isCurrent; final bool isCurrent;
final String accountName; final String accountName;

View file

@ -73,7 +73,7 @@ class RestoreOptionsPage extends BasePage {
await restoreFromQRViewModel.create(restoreWallet: restoreWallet); await restoreFromQRViewModel.create(restoreWallet: restoreWallet);
if (restoreFromQRViewModel.state is FailureState) { if (restoreFromQRViewModel.state is FailureState) {
_onWalletCreateFailure(context, _onWalletCreateFailure(context,
'Create wallet state: ${restoreFromQRViewModel.state.runtimeType.toString()}'); 'Create wallet state: ${(restoreFromQRViewModel.state as FailureState).error}');
} }
} catch (e) { } catch (e) {
_onWalletCreateFailure(context, e.toString()); _onWalletCreateFailure(context, e.toString());

View file

@ -50,6 +50,14 @@ class PrivacyPage extends BasePage {
onValueChange: (BuildContext _, bool value) { onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setShouldSaveRecipientAddress(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) if (DeviceInfo.instance.isMobile)
SettingsSwitcherCell( SettingsSwitcherCell(
title: S.current.prevent_screenshots, title: S.current.prevent_screenshots,

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; 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/cake_2fa_preset_options.dart';
import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
@ -42,6 +43,7 @@ abstract class SettingsStoreBase with Store {
required FiatCurrency initialFiatCurrency, required FiatCurrency initialFiatCurrency,
required BalanceDisplayMode initialBalanceDisplayMode, required BalanceDisplayMode initialBalanceDisplayMode,
required bool initialSaveRecipientAddress, required bool initialSaveRecipientAddress,
required AutoGenerateSubaddressStatus initialAutoGenerateSubaddressStatus,
required bool initialAppSecure, required bool initialAppSecure,
required bool initialDisableBuy, required bool initialDisableBuy,
required bool initialDisableSell, required bool initialDisableSell,
@ -87,6 +89,7 @@ abstract class SettingsStoreBase with Store {
fiatCurrency = initialFiatCurrency, fiatCurrency = initialFiatCurrency,
balanceDisplayMode = initialBalanceDisplayMode, balanceDisplayMode = initialBalanceDisplayMode,
shouldSaveRecipientAddress = initialSaveRecipientAddress, shouldSaveRecipientAddress = initialSaveRecipientAddress,
autoGenerateSubaddressStatus = initialAutoGenerateSubaddressStatus,
fiatApiMode = initialFiatMode, fiatApiMode = initialFiatMode,
allowBiometricalAuthentication = initialAllowBiometricalAuthentication, allowBiometricalAuthentication = initialAllowBiometricalAuthentication,
selectedCake2FAPreset = initialCake2FAPresetOptions, selectedCake2FAPreset = initialCake2FAPresetOptions,
@ -197,6 +200,11 @@ abstract class SettingsStoreBase with Store {
(bool disableSell) => (bool disableSell) =>
sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell)); sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
reaction(
(_) => autoGenerateSubaddressStatus,
(AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt(
PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus.value));
reaction( reaction(
(_) => fiatApiMode, (_) => fiatApiMode,
(FiatApiMode mode) => (FiatApiMode mode) =>
@ -337,6 +345,7 @@ abstract class SettingsStoreBase with Store {
static const defaultPinLength = 4; static const defaultPinLength = 4;
static const defaultActionsMode = 11; static const defaultActionsMode = 11;
static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes; static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes;
static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized;
@observable @observable
FiatCurrency fiatCurrency; FiatCurrency fiatCurrency;
@ -359,6 +368,9 @@ abstract class SettingsStoreBase with Store {
@observable @observable
bool shouldSaveRecipientAddress; bool shouldSaveRecipientAddress;
@observable
AutoGenerateSubaddressStatus autoGenerateSubaddressStatus;
@observable @observable
bool isAppSecure; bool isAppSecure;
@ -602,7 +614,12 @@ abstract class SettingsStoreBase with Store {
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
final deviceName = await _getDeviceName() ?? ''; final deviceName = await _getDeviceName() ?? '';
final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; 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>{}; final nodes = <WalletType, Node>{};
if (moneroNode != null) { if (moneroNode != null) {
@ -640,6 +657,7 @@ abstract class SettingsStoreBase with Store {
initialFiatCurrency: currentFiatCurrency, initialFiatCurrency: currentFiatCurrency,
initialBalanceDisplayMode: currentBalanceDisplayMode, initialBalanceDisplayMode: currentBalanceDisplayMode,
initialSaveRecipientAddress: shouldSaveRecipientAddress, initialSaveRecipientAddress: shouldSaveRecipientAddress,
initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus,
initialAppSecure: isAppSecure, initialAppSecure: isAppSecure,
initialDisableBuy: disableBuy, initialDisableBuy: disableBuy,
initialDisableSell: disableSell, initialDisableSell: disableSell,
@ -709,6 +727,13 @@ abstract class SettingsStoreBase with Store {
priority[WalletType.ethereum]!; priority[WalletType.ethereum]!;
} }
final generateSubaddresses =
sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey);
autoGenerateSubaddressStatus = generateSubaddresses != null
? AutoGenerateSubaddressStatus.deserialize(raw: generateSubaddresses)
: defaultAutoGenerateSubaddressStatus;
balanceDisplayMode = BalanceDisplayMode.deserialize( balanceDisplayMode = BalanceDisplayMode.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!);
shouldSaveRecipientAddress = shouldSaveRecipientAddress =
@ -719,8 +744,6 @@ abstract class SettingsStoreBase with Store {
numberOfFailedTokenTrials = numberOfFailedTokenTrials =
sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ??
shouldSaveRecipientAddress;
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy; disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell; disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;

View file

@ -1,4 +1,5 @@
import 'dart:async'; 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/contact_base.dart';
import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/store/settings_store.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/entities/contact.dart';
import 'package:cake_wallet/utils/mobx.dart'; import 'package:cake_wallet/utils/mobx.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:collection/collection.dart';
part 'contact_list_view_model.g.dart'; part 'contact_list_view_model.g.dart';
@ -20,12 +22,26 @@ abstract class ContactListViewModelBase with Store {
ContactListViewModelBase(this.contactSource, this.walletInfoSource, ContactListViewModelBase(this.contactSource, this.walletInfoSource,
this._currency, this.settingsStore) this._currency, this.settingsStore)
: contacts = ObservableList<ContactRecord>(), : contacts = ObservableList<ContactRecord>(),
walletContacts = [] { walletContacts = [],
isAutoGenerateEnabled =
settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled {
walletInfoSource.values.forEach((info) { walletInfoSource.values.forEach((info) {
if (info.addresses?.isNotEmpty ?? false) { if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) {
info.addresses?.forEach((address, label) { info.addressInfos!.forEach((key, value) {
final name = label.isNotEmpty ? info.name + ' ($label)' : info.name; 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( walletContacts.add(WalletContact(
address, address,
name, name,
@ -40,6 +56,11 @@ abstract class ContactListViewModelBase with Store {
initialFire: true); initialFire: true);
} }
String _createName(String walletName, String label) {
return label.isNotEmpty ? '$walletName ($label)' : walletName;
}
final bool isAutoGenerateEnabled;
final Box<Contact> contactSource; final Box<Contact> contactSource;
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
final ObservableList<ContactRecord> contacts; 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/entities/exchange_api_mode.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
@ -235,6 +236,10 @@ abstract class DashboardViewModelBase with Store {
@computed @computed
double get price => balanceViewModel.price; double get price => balanceViewModel.price;
@computed
bool get isAutoGenerateSubaddressesEnabled =>
settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled;
@computed @computed
List<ActionListItem> get items { List<ActionListItem> get items {
final _items = <ActionListItem>[]; 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/entities/exchange_api_mode.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/store/settings_store.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_base.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -14,11 +18,27 @@ abstract class PrivacySettingsViewModelBase with Store {
PrivacySettingsViewModelBase(this._settingsStore, this._wallet); PrivacySettingsViewModelBase(this._settingsStore, this._wallet);
final SettingsStore _settingsStore; final SettingsStore _settingsStore;
final WalletBase _wallet; final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
@computed @computed
ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus; 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 @computed
bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress; bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress;

View file

@ -682,5 +682,7 @@
"support_title_other_links": "روابط دعم أخرى", "support_title_other_links": "روابط دعم أخرى",
"support_description_other_links": "انضم إلى مجتمعاتنا أو تصل إلينا شركائنا من خلال أساليب أخرى", "support_description_other_links": "انضم إلى مجتمعاتنا أو تصل إلينا شركائنا من خلال أساليب أخرى",
"select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", "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 (препоръчително)", "setup_totp_recommended": "Настройка на TOTP (препоръчително)",
"disable_buy": "Деактивирайте действието за покупка", "disable_buy": "Деактивирайте действието за покупка",
"disable_sell": "Деактивирайте действието за продажба", "disable_sell": "Деактивирайте действието за продажба",
"auto_generate_subaddresses": "Автоматично генериране на подадреси",
"cake_2fa_preset": "Торта 2FA Preset", "cake_2fa_preset": "Торта 2FA Preset",
"narrow": "Тесен", "narrow": "Тесен",
"normal": "нормално", "normal": "нормално",

View file

@ -626,6 +626,7 @@
"setup_totp_recommended": "Nastavit TOTP (doporučeno)", "setup_totp_recommended": "Nastavit TOTP (doporučeno)",
"disable_buy": "Zakázat akci nákupu", "disable_buy": "Zakázat akci nákupu",
"disable_sell": "Zakázat akci prodeje", "disable_sell": "Zakázat akci prodeje",
"auto_generate_subaddresses": "Automaticky generovat podadresy",
"cake_2fa_preset": "Předvolba Cake 2FA", "cake_2fa_preset": "Předvolba Cake 2FA",
"narrow": "Úzký", "narrow": "Úzký",
"normal": "Normální", "normal": "Normální",

View file

@ -640,6 +640,7 @@
"high_contrast_theme": "Kontrastreiches Thema", "high_contrast_theme": "Kontrastreiches Thema",
"matrix_green_dark_theme": "Matrix Green Dark Theme", "matrix_green_dark_theme": "Matrix Green Dark Theme",
"monero_light_theme": "Monero Light-Thema", "monero_light_theme": "Monero Light-Thema",
"auto_generate_subaddresses": "Unteradressen automatisch generieren",
"cake_2fa_preset" : "Cake 2FA-Voreinstellung", "cake_2fa_preset" : "Cake 2FA-Voreinstellung",
"narrow": "Eng", "narrow": "Eng",
"normal": "Normal", "normal": "Normal",

View file

@ -568,6 +568,7 @@
"privacy": "Privacy", "privacy": "Privacy",
"display_settings": "Display settings", "display_settings": "Display settings",
"other_settings": "Other settings", "other_settings": "Other settings",
"auto_generate_subaddresses": "Auto generate subaddresses",
"require_pin_after": "Require PIN after", "require_pin_after": "Require PIN after",
"always": "Always", "always": "Always",
"minutes_to_pin_code": "${minute} minutes", "minutes_to_pin_code": "${minute} minutes",

View file

@ -640,6 +640,7 @@
"high_contrast_theme": "Tema de alto contraste", "high_contrast_theme": "Tema de alto contraste",
"matrix_green_dark_theme": "Matrix verde oscuro tema", "matrix_green_dark_theme": "Matrix verde oscuro tema",
"monero_light_theme": "Tema ligero de Monero", "monero_light_theme": "Tema ligero de Monero",
"auto_generate_subaddresses": "Generar subdirecciones automáticamente",
"cake_2fa_preset": "Pastel 2FA preestablecido", "cake_2fa_preset": "Pastel 2FA preestablecido",
"narrow": "Angosto", "narrow": "Angosto",
"normal": "Normal", "normal": "Normal",

View file

@ -640,6 +640,7 @@
"high_contrast_theme": "Thème à contraste élevé", "high_contrast_theme": "Thème à contraste élevé",
"matrix_green_dark_theme": "Thème Matrix Green Dark", "matrix_green_dark_theme": "Thème Matrix Green Dark",
"monero_light_theme": "Thème de lumière Monero", "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", "cake_2fa_preset": "Gâteau 2FA prédéfini",
"narrow": "Étroit", "narrow": "Étroit",
"normal": "Normal", "normal": "Normal",

View file

@ -618,6 +618,7 @@
"high_contrast_theme": "Babban Jigon Kwatance", "high_contrast_theme": "Babban Jigon Kwatance",
"matrix_green_dark_theme": "Matrix Green Dark Jigo", "matrix_green_dark_theme": "Matrix Green Dark Jigo",
"monero_light_theme": "Jigon Hasken Monero", "monero_light_theme": "Jigon Hasken Monero",
"auto_generate_subaddresses": "Saɓaƙa subaddresses ta kai tsaye",
"cake_2fa_preset": "Cake 2FA saiti", "cake_2fa_preset": "Cake 2FA saiti",
"narrow": "kunkuntar", "narrow": "kunkuntar",
"normal": "Na al'ada", "normal": "Na al'ada",

View file

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

View file

@ -640,6 +640,7 @@
"high_contrast_theme": "Tema visokog kontrasta", "high_contrast_theme": "Tema visokog kontrasta",
"matrix_green_dark_theme": "Matrix Green Dark Theme", "matrix_green_dark_theme": "Matrix Green Dark Theme",
"monero_light_theme": "Monero lagana tema", "monero_light_theme": "Monero lagana tema",
"auto_generate_subaddresses": "Automatski generirajte podadrese",
"cake_2fa_preset": "Cake 2FA Preset", "cake_2fa_preset": "Cake 2FA Preset",
"narrow": "Usko", "narrow": "Usko",
"normal": "Normalno", "normal": "Normalno",

View file

@ -622,6 +622,7 @@
"setup_totp_recommended": "Siapkan TOTP (Disarankan)", "setup_totp_recommended": "Siapkan TOTP (Disarankan)",
"disable_buy": "Nonaktifkan tindakan beli", "disable_buy": "Nonaktifkan tindakan beli",
"disable_sell": "Nonaktifkan aksi jual", "disable_sell": "Nonaktifkan aksi jual",
"auto_generate_subaddresses": "Menghasilkan subalamat secara otomatis",
"cake_2fa_preset": "Preset Kue 2FA", "cake_2fa_preset": "Preset Kue 2FA",
"narrow": "Sempit", "narrow": "Sempit",
"normal": "Normal", "normal": "Normal",

View file

@ -634,6 +634,7 @@
"setup_totp_recommended": "Imposta TOTP (consigliato)", "setup_totp_recommended": "Imposta TOTP (consigliato)",
"disable_buy": "Disabilita l'azione di acquisto", "disable_buy": "Disabilita l'azione di acquisto",
"disable_sell": "Disabilita l'azione di vendita", "disable_sell": "Disabilita l'azione di vendita",
"auto_generate_subaddresses": "Genera automaticamente sottindirizzi",
"cake_2fa_preset": "Torta 2FA Preset", "cake_2fa_preset": "Torta 2FA Preset",
"narrow": "Stretto", "narrow": "Stretto",
"normal": "Normale", "normal": "Normale",

View file

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

View file

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

View file

@ -684,5 +684,6 @@
"support_title_other_links": "အခြားအထောက်အပံ့လင့်များ", "support_title_other_links": "အခြားအထောက်အပံ့လင့်များ",
"support_description_other_links": "ကျွန်ုပ်တို့၏လူမှုအသိုင်းအဝိုင်းများသို့ 0 င်ရောက်ပါ", "support_description_other_links": "ကျွန်ုပ်တို့၏လူမှုအသိုင်းအဝိုင်းများသို့ 0 င်ရောက်ပါ",
"select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", "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)", "setup_totp_recommended": "TOTP instellen (aanbevolen)",
"disable_buy": "Koopactie uitschakelen", "disable_buy": "Koopactie uitschakelen",
"disable_sell": "Verkoopactie uitschakelen", "disable_sell": "Verkoopactie uitschakelen",
"auto_generate_subaddresses": "Automatisch subadressen genereren",
"cake_2fa_preset": "Taart 2FA Voorinstelling", "cake_2fa_preset": "Taart 2FA Voorinstelling",
"narrow": "Smal", "narrow": "Smal",
"normal": "Normaal", "normal": "Normaal",

View file

@ -634,6 +634,7 @@
"setup_totp_recommended": "Skonfiguruj TOTP (zalecane)", "setup_totp_recommended": "Skonfiguruj TOTP (zalecane)",
"disable_buy": "Wyłącz akcję kupna", "disable_buy": "Wyłącz akcję kupna",
"disable_sell": "Wyłącz akcję sprzedaży", "disable_sell": "Wyłącz akcję sprzedaży",
"auto_generate_subaddresses": "Automatycznie generuj podadresy",
"cake_2fa_preset": "Ciasto 2FA Preset", "cake_2fa_preset": "Ciasto 2FA Preset",
"narrow": "Wąski", "narrow": "Wąski",
"normal": "Normalna", "normal": "Normalna",

View file

@ -633,6 +633,7 @@
"setup_totp_recommended": "Configurar TOTP (recomendado)", "setup_totp_recommended": "Configurar TOTP (recomendado)",
"disable_buy": "Desativar ação de compra", "disable_buy": "Desativar ação de compra",
"disable_sell": "Desativar ação de venda", "disable_sell": "Desativar ação de venda",
"auto_generate_subaddresses": "Gerar subendereços automaticamente",
"cake_2fa_preset": "Predefinição de bolo 2FA", "cake_2fa_preset": "Predefinição de bolo 2FA",
"narrow": "Estreito", "narrow": "Estreito",
"normal": "Normal", "normal": "Normal",

View file

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

View file

@ -684,5 +684,6 @@
"support_title_other_links": "ลิงค์สนับสนุนอื่น ๆ", "support_title_other_links": "ลิงค์สนับสนุนอื่น ๆ",
"support_description_other_links": "เข้าร่วมชุมชนของเราหรือเข้าถึงเราพันธมิตรของเราผ่านวิธีการอื่น ๆ", "support_description_other_links": "เข้าร่วมชุมชนของเราหรือเข้าถึงเราพันธมิตรของเราผ่านวิธีการอื่น ๆ",
"select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", "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)", "setup_totp_recommended": "TOTP'yi kurun (Önerilir)",
"disable_buy": "Satın alma işlemini devre dışı bırak", "disable_buy": "Satın alma işlemini devre dışı bırak",
"disable_sell": "Satış 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ı", "cake_2fa_preset": "Kek 2FA Ön Ayarı",
"narrow": "Dar", "narrow": "Dar",
"normal": "Normal", "normal": "Normal",

View file

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

View file

@ -678,5 +678,6 @@
"support_title_other_links": "دوسرے سپورٹ لنکس", "support_title_other_links": "دوسرے سپورٹ لنکس",
"support_description_other_links": "ہماری برادریوں میں شامل ہوں یا دوسرے طریقوں سے ہمارے شراکت داروں تک پہنچیں", "support_description_other_links": "ہماری برادریوں میں شامل ہوں یا دوسرے طریقوں سے ہمارے شراکت داروں تک پہنچیں",
"select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", "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", "matrix_green_dark_theme": "Matrix Green Dark Akori",
"monero_light_theme": "Monero Light Akori", "monero_light_theme": "Monero Light Akori",
"select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.", "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": "矩阵绿暗主题", "matrix_green_dark_theme": "矩阵绿暗主题",
"monero_light_theme": "门罗币浅色主题", "monero_light_theme": "门罗币浅色主题",
"select_destination": "请选择备份文件的目的地。", "select_destination": "请选择备份文件的目的地。",
"save_to_downloads": "保存到下载" "save_to_downloads": "保存到下载",
"auto_generate_subaddresses": "自动生成子辅助"
} }

View file

@ -1,15 +1,6 @@
import 'dart:convert'; import 'utils/translation/arb_file_utils.dart';
import 'dart:io'; import 'utils/translation/translation_constants.dart';
import 'utils/translation/translation_utils.dart';
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();
void main(List<String> args) async { void main(List<String> args) async {
if (args.length != 2) { if (args.length != 2) {
@ -23,44 +14,9 @@ void main(List<String> args) async {
print('Appending "$name": "$text"'); print('Appending "$name": "$text"');
for (var lang in langs) { for (var lang in langs) {
final fileName = getFileName(lang); final fileName = getArbFileName(lang);
final translation = await getTranslation(text, 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;
}