diff --git a/PRIVACY.md b/PRIVACY.md index 88f180c5e..76cfcc4d3 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,6 +1,6 @@ Privacy Policy -Last modified: August 9, 2023 +Last modified: January 24, 2024 Introduction ============ @@ -112,12 +112,12 @@ Data Security In any situation, Cake Labs takes no responsibility for interception of personal data by any outside individual, group, corporation, or institution. You should understand this and take any and all appropriate actions to secure your own data. -Links to Other Websites ------------------------ +Other Websites and Third-Party Services +--------------------------------------- The App may contain links to other websites that are not operated by us. If you click on a Third-Party Service link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. We have no control over and assume no responsibility for the content, privacy policies or practices of any third-party sites or services. - The App includes several optional Third-Party Services, which may not be available to all users. If you use Third-Party Services, you must agree to their respective Privacy Policies. + The App includes several optional Third-Party Services, which may not be available to all users. If you use Third-Party Services, you must agree to their respective Privacy Policies. When using certain optional features in the app such as buying and selling, you may be asked to provide information to a Third-Party Service. You will need to read and accept the privacy policy for that third party. This Third-Party Service may ask for your name, your photo ID, your social security number or other similar number, mailing address, cryptocurrency address, or other information. They may ask you to take a selfie image. Information shared with a Third-Party Service is subject to their respective Privacy Policies. Changes to Our Privacy Policy ----------------------------- diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 392771ab0..f2cd63904 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,40 +1,70 @@ import 'dart:convert'; +import 'package:bitbox/bitbox.dart' as bitbox; class BitcoinAddressRecord { - BitcoinAddressRecord(this.address, - {required this.index, this.isHidden = false, bool isUsed = false}) - : _isUsed = isUsed; + BitcoinAddressRecord( + this.address, { + required this.index, + this.isHidden = false, + int txCount = 0, + int balance = 0, + String name = '', + bool isUsed = false, + }) : _txCount = txCount, + _balance = balance, + _name = name, + _isUsed = isUsed; factory BitcoinAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; - return BitcoinAddressRecord( - decoded['address'] as String, - index: decoded['index'] as int, - isHidden: decoded['isHidden'] as bool? ?? false, - isUsed: decoded['isUsed'] as bool? ?? false); + return BitcoinAddressRecord(decoded['address'] as String, + index: decoded['index'] as int, + isHidden: decoded['isHidden'] as bool? ?? false, + isUsed: decoded['isUsed'] as bool? ?? false, + txCount: decoded['txCount'] as int? ?? 0, + name: decoded['name'] as String? ?? '', + balance: decoded['balance'] as int? ?? 0); } - @override - bool operator ==(Object o) => - o is BitcoinAddressRecord && address == o.address; - final String address; final bool isHidden; final int index; + int _txCount; + int _balance; + String _name; + bool _isUsed; + + int get txCount => _txCount; + + String get name => _name; + + int get balance => _balance; + + set txCount(int value) => _txCount = value; + + set balance(int value) => _balance = value; + bool get isUsed => _isUsed; + void setAsUsed() => _isUsed = true; + void setNewName(String label) => _name = label; + + @override + bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address; + @override int get hashCode => address.hashCode; - bool _isUsed; + String get cashAddr => bitbox.Address.toCashAddress(address); - void setAsUsed() => _isUsed = true; - - String toJSON() => - json.encode({ + String toJSON() => json.encode({ 'address': address, 'index': index, 'isHidden': isHidden, - 'isUsed': isUsed}); + 'txCount': txCount, + 'name': name, + 'isUsed': isUsed, + 'balance': balance, + }); } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 2c66d02fe..9cdb78f2d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -47,6 +47,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) .derivePath("m/0'/1"), networkType: networkType); + autorun((_) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + }); } static Future create({ diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 634935669..308947b4e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -63,6 +63,7 @@ abstract class ElectrumWalletBase _password = password, _feeRates = [], _isTransactionUpdating = false, + isEnabledAutoGenerateSubaddress = true, unspentCoins = [], _scripthashesUpdateSubject = {}, balance = ObservableMap.of(currency != null @@ -87,6 +88,10 @@ abstract class ElectrumWalletBase final bitcoin.HDWallet hd; final String mnemonic; + @override + @observable + bool isEnabledAutoGenerateSubaddress; + late ElectrumClient electrumClient; Box unspentCoinsInfo; @@ -588,38 +593,66 @@ abstract class ElectrumWalletBase Future> fetchTransactions() async { final addressHashes = {}; final normalizedHistories = >[]; + final newTxCounts = {}; + walletAddresses.addresses.forEach((addressRecord) { final sh = scriptHash(addressRecord.address, networkType: networkType); addressHashes[sh] = addressRecord; + newTxCounts[sh] = 0; }); - final histories = addressHashes.keys.map((scriptHash) => - electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history})); - final historyResults = await Future.wait(histories); - historyResults.forEach((history) { - history.entries.forEach((historyItem) { - if (historyItem.value.isNotEmpty) { - final address = addressHashes[historyItem.key]; - address?.setAsUsed(); - normalizedHistories.addAll(historyItem.value); - } + + try { + final histories = addressHashes.keys.map((scriptHash) => + electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history})); + final historyResults = await Future.wait(histories); + + + + historyResults.forEach((history) { + history.entries.forEach((historyItem) { + if (historyItem.value.isNotEmpty) { + final address = addressHashes[historyItem.key]; + address?.setAsUsed(); + newTxCounts[historyItem.key] = historyItem.value.length; + normalizedHistories.addAll(historyItem.value); + } + }); }); - }); - final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) { - try { - return fetchTransactionInfo( - hash: transaction['tx_hash'] as String, height: transaction['height'] as int); - } catch (_) { - return Future.value(null); + + for (var sh in addressHashes.keys) { + var balanceData = await electrumClient.getBalance(sh); + var addressRecord = addressHashes[sh]; + if (addressRecord != null) { + addressRecord.balance = balanceData['confirmed'] as int? ?? 0; + } } - })); - return historiesWithDetails - .fold>({}, (acc, tx) { - if (tx == null) { + + + addressHashes.forEach((sh, addressRecord) { + addressRecord.txCount = newTxCounts[sh] ?? 0; + }); + + final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) { + try { + return fetchTransactionInfo( + hash: transaction['tx_hash'] as String, height: transaction['height'] as int); + } catch (_) { + return Future.value(null); + } + })); + + return historiesWithDetails.fold>( + {}, (acc, tx) { + if (tx == null) { + return acc; + } + acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; return acc; - } - acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; - return acc; - }); + }); + } catch (e) { + print(e.toString()); + return {}; + } } Future updateTransactions() async { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index c9e4f8200..a60a7e8ec 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,5 +1,5 @@ -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/script_hash.dart'; @@ -10,8 +10,7 @@ import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; -class ElectrumWalletAddresses = ElectrumWalletAddressesBase - with _$ElectrumWalletAddresses; +class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase(WalletInfo walletInfo, @@ -22,19 +21,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialAddresses, int initialRegularAddressIndex = 0, int initialChangeAddressIndex = 0}) - : addresses = ObservableList.of( - (initialAddresses ?? []).toSet()), - receiveAddresses = ObservableList.of( - (initialAddresses ?? []) + : addresses = ObservableList.of((initialAddresses ?? []).toSet()), + receiveAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), - changeAddresses = ObservableList.of( - (initialAddresses ?? []) + .toSet()), + changeAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), + .toSet()), currentReceiveAddressIndex = initialRegularAddressIndex, currentChangeAddressIndex = initialChangeAddressIndex, - super(walletInfo); + super(walletInfo); static const defaultReceiveAddressesCount = 22; static const defaultChangeAddressesCount = 17; @@ -42,6 +38,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static String toCashAddr(String address) => bitbox.Address.toCashAddress(address); + static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address); + final ObservableList addresses; final ObservableList receiveAddresses; final ObservableList changeAddresses; @@ -53,41 +51,67 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override @computed String get address { - if (receiveAddresses.isEmpty) { - final address = generateNewAddress().address; - return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(address) : address; - } - final receiveAddress = receiveAddresses.first.address; + if (isEnabledAutoGenerateSubaddress) { + if (receiveAddresses.isEmpty) { + final newAddress = generateNewAddress().address; + return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress; + } + final receiveAddress = receiveAddresses.first.address; - return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress; + return walletInfo.type == WalletType.bitcoinCash + ? toCashAddr(receiveAddress) + : receiveAddress; + } else { + final receiveAddress = (receiveAddresses.first.address != addresses.first.address && + previousAddressRecord != null) + ? previousAddressRecord!.address + : addresses.first.address; + + return walletInfo.type == WalletType.bitcoinCash + ? toCashAddr(receiveAddress) + : receiveAddress; + } + } + + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @override + set address(String addr) { + if (addr.startsWith('bitcoincash:')) { + addr = toLegacy(addr); + } + final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == addr); + + previousAddressRecord = addressRecord; + receiveAddresses.remove(addressRecord); + receiveAddresses.insert(0, addressRecord); } @override String get primaryAddress => getAddress(index: 0, hd: mainHd); - @override - set address(String addr) => null; - int currentReceiveAddressIndex; int currentChangeAddressIndex; - @computed - int get totalCountOfReceiveAddresses => - addresses.fold(0, (acc, addressRecord) { - if (!addressRecord.isHidden) { - return acc + 1; - } - return acc; - }); + @observable + BitcoinAddressRecord? previousAddressRecord; @computed - int get totalCountOfChangeAddresses => - addresses.fold(0, (acc, addressRecord) { - if (addressRecord.isHidden) { - return acc + 1; - } - return acc; - }); + int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) { + if (!addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); + + @computed + int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) { + if (addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); Future discoverAddresses() async { await _discoverAddresses(mainHd, false); @@ -117,11 +141,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (changeAddresses.isEmpty) { final newAddresses = await _createNewAddresses(gap, - hd: sideHd, - startIndex: totalCountOfChangeAddresses > 0 - ? totalCountOfChangeAddresses - 1 - : 0, - isHidden: true); + hd: sideHd, + startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0, + isHidden: true); _addAddresses(newAddresses); } @@ -135,14 +157,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } - BitcoinAddressRecord generateNewAddress( - {bitcoin.HDWallet? hd, bool isHidden = false}) { - currentReceiveAddressIndex += 1; - // FIX-ME: Check logic for whichi HD should be used here ??? - final address = BitcoinAddressRecord( - getAddress(index: currentReceiveAddressIndex, hd: hd ?? sideHd), - index: currentReceiveAddressIndex, - isHidden: isHidden); + BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) { + final isHidden = hd == sideHd; + + final newAddressIndex = addresses.fold( + 0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc); + + final address = BitcoinAddressRecord(getAddress(index: newAddressIndex, hd: hd ?? sideHd), + index: newAddressIndex, isHidden: isHidden, name: label ?? ''); addresses.add(address); return address; } @@ -160,20 +182,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } } + @action + void updateAddress(String address, String label) { + if (address.startsWith('bitcoincash:')) { + address = toLegacy(address); + } + final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == address); + addressRecord.setNewName(label); + final index = addresses.indexOf(addressRecord); + addresses.remove(addressRecord); + addresses.insert(index, addressRecord); + } + @action void updateReceiveAddresses() { receiveAddresses.removeRange(0, receiveAddresses.length); - final newAdresses = addresses - .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); - receiveAddresses.addAll(newAdresses); + final newAddresses = + addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); + receiveAddresses.addAll(newAddresses); } @action void updateChangeAddresses() { changeAddresses.removeRange(0, changeAddresses.length); - final newAdresses = addresses - .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); - changeAddresses.addAll(newAdresses); + final newAddresses = + addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); + changeAddresses.addAll(newAddresses); } Future _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async { @@ -181,20 +215,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List addrs; if (addresses.isNotEmpty) { - addrs = addresses - .where((addr) => addr.isHidden == isHidden) - .toList(); + addrs = addresses.where((addr) => addr.isHidden == isHidden).toList(); } else { addrs = await _createNewAddresses( - isHidden - ? defaultChangeAddressesCount - : defaultReceiveAddressesCount, + isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount, startIndex: 0, hd: hd, isHidden: isHidden); } - while(hasAddrUse) { + while (hasAddrUse) { final addr = addrs.last.address; hasAddrUse = await _hasAddressUsed(addr); @@ -204,11 +234,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final start = addrs.length; final count = start + gap; - final batch = await _createNewAddresses( - count, - startIndex: start, - hd: hd, - isHidden: isHidden); + final batch = await _createNewAddresses(count, startIndex: start, hd: hd, isHidden: isHidden); addrs.addAll(batch); } @@ -232,21 +258,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (countOfReceiveAddresses < defaultReceiveAddressesCount) { final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; - final newAddresses = await _createNewAddresses( - addressesCount, - startIndex: countOfReceiveAddresses, - hd: mainHd, - isHidden: false); + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false); addresses.addAll(newAddresses); } if (countOfHiddenAddresses < defaultChangeAddressesCount) { final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; - final newAddresses = await _createNewAddresses( - addressesCount, - startIndex: countOfHiddenAddresses, - hd: sideHd, - isHidden: true); + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true); addresses.addAll(newAddresses); } } @@ -256,10 +276,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final list = []; for (var i = startIndex; i < count + startIndex; i++) { - final address = BitcoinAddressRecord( - getAddress(index: i, hd: hd), - index: i, - isHidden: isHidden); + final address = + BitcoinAddressRecord(getAddress(index: i, hd: hd), index: i, isHidden: isHidden); list.add(address); } @@ -278,4 +296,4 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final transactionHistory = await electrumClient.getHistory(sh); return transactionHistory.isNotEmpty; } -} \ No newline at end of file +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 6bf1c5735..222e95acc 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -51,6 +51,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { .fromSeed(seedBytes, network: networkType) .derivePath("m/0'/1"), networkType: networkType,); + autorun((_) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + }); } static Future create({ diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 7ce21b82f..eee43a94d 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -58,6 +58,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { sideHd: bitcoin.HDWallet.fromSeed(seedBytes) .derivePath("m/44'/145'/0'/1"), networkType: networkType); + autorun((_) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + }); } diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index 6c3739c11..70fa9daa3 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -202,11 +202,15 @@ I/flutter ( 4474): Gas Used: 53000 Future fetchERC20Balances( EthereumAddress userAddress, String contractAddress) async { final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); - final balance = await erc20.balanceOf(userAddress); + try { + final balance = await erc20.balanceOf(userAddress); - int exponent = (await erc20.decimals()).toInt(); + int exponent = (await erc20.decimals()).toInt(); - return ERC20Balance(balance, exponent: exponent); + return ERC20Balance(balance, exponent: exponent); + } catch (_) { + return ERC20Balance(BigInt.zero); + } } Future getErc20Token(String contractAddress) async { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index aa89b2747..05f888b2b 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -63,9 +63,17 @@ class CWBitcoin extends Bitcoin { } @override - Future generateNewAddress(Object wallet) async { + Future generateNewAddress(Object wallet, String label) async { final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.walletAddresses.generateNewAddress(); + await bitcoinWallet.walletAddresses.generateNewAddress(label: label); + await wallet.save(); + } + + @override + Future updateAddress(Object wallet,String address, String label) async { + final bitcoinWallet = wallet as ElectrumWallet; + bitcoinWallet.walletAddresses.updateAddress(address, label); + await wallet.save(); } @override @@ -100,6 +108,21 @@ class CWBitcoin extends Bitcoin { .toList(); } + @override + @computed + List getSubAddresses(Object wallet) { + final electrumWallet = wallet as ElectrumWallet; + return electrumWallet.walletAddresses.addresses + .map((BitcoinAddressRecord addr) => ElectrumSubAddress( + id: addr.index, + name: addr.name, + address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address, + txCount: addr.txCount, + balance: addr.balance, + isChange: addr.isHidden)) + .toList(); + } + @override String getAddress(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index 78a5277ce..8e2d58d11 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -46,6 +46,8 @@ class DFXBuyProvider extends BuyProvider { return 'XMR'; case WalletType.ethereum: return 'ETH'; + case WalletType.polygon: + return 'MATIC'; default: throw Exception("WalletType is not available for DFX ${wallet.type}"); } @@ -61,6 +63,8 @@ class DFXBuyProvider extends BuyProvider { return 'Monero'; case WalletType.ethereum: return 'Ethereum'; + case WalletType.polygon: + return 'Polygon'; default: throw Exception("WalletType is not available for DFX ${wallet.type}"); } @@ -141,6 +145,7 @@ class DFXBuyProvider extends BuyProvider { String getSignature(String message) { switch (wallet.type) { case WalletType.ethereum: + case WalletType.polygon: return wallet.signMessage(message); case WalletType.monero: case WalletType.litecoin: diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index c072bf65e..a99aef31d 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -72,11 +72,11 @@ class AuthService with Store { void saveLastAuthTime() { int timestamp = DateTime.now().millisecondsSinceEpoch; - sharedPreferences.setInt(PreferencesKey.lastAuthTimeMilliseconds, timestamp); + secureStorage.write(key: SecureKey.lastAuthTimeMilliseconds, value: timestamp.toString()); } - bool requireAuth() { - final timestamp = sharedPreferences.getInt(PreferencesKey.lastAuthTimeMilliseconds); + Future requireAuth() async { + final timestamp = int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0'); final duration = _durationToRequireAuth(timestamp ?? 0); final requiredPinInterval = settingsStore.pinTimeOutDuration; @@ -100,7 +100,7 @@ class AuthService with Store { 'Either route or onAuthSuccess param must be passed.'); if (!conditionToDetermineIfToUse2FA) { - if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { + if (!(await requireAuth()) && !_alwaysAuthenticateRoutes.contains(route)) { if (onAuthSuccess != null) { onAuthSuccess(true); } else { diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 7dd0b50f3..9b5c4c8db 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -213,8 +213,6 @@ class BackupService { final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?; final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; - final allowBiometricalAuthentication = - data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; final currentBitcoinElectrumSererId = data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?; @@ -227,23 +225,6 @@ class BackupService { data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?; final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?; - final selectedCake2FAPreset = data[PreferencesKey.selectedCake2FAPreset] as int?; - final shouldRequireTOTP2FAForAccessingWallet = - data[PreferencesKey.shouldRequireTOTP2FAForAccessingWallet] as bool?; - final shouldRequireTOTP2FAForSendsToContact = - data[PreferencesKey.shouldRequireTOTP2FAForSendsToContact] as bool?; - final shouldRequireTOTP2FAForSendsToNonContact = - data[PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact] as bool?; - final shouldRequireTOTP2FAForSendsToInternalWallets = - data[PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets] as bool?; - final shouldRequireTOTP2FAForExchangesToInternalWallets = - data[PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets] as bool?; - final shouldRequireTOTP2FAForAddingContacts = - data[PreferencesKey.shouldRequireTOTP2FAForAddingContacts] as bool?; - final shouldRequireTOTP2FAForCreatingNewWallets = - data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?; - final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = - data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?; final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; @@ -294,14 +275,7 @@ class BackupService { if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); - - if (DeviceInfo.instance.isDesktop) { - await _sharedPreferences.setBool(PreferencesKey.allowBiometricalAuthenticationKey, false); - } else if (allowBiometricalAuthentication != null) { - await _sharedPreferences.setBool( - PreferencesKey.allowBiometricalAuthenticationKey, allowBiometricalAuthentication); - } - + if (currentBitcoinElectrumSererId != null) await _sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId); @@ -344,43 +318,6 @@ class BackupService { await _sharedPreferences.setInt( PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority); - if (selectedCake2FAPreset != null) - await _sharedPreferences.setInt(PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset); - - if (shouldRequireTOTP2FAForAccessingWallet != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet, - shouldRequireTOTP2FAForAccessingWallet); - - if (shouldRequireTOTP2FAForSendsToContact != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact, - shouldRequireTOTP2FAForSendsToContact); - - if (shouldRequireTOTP2FAForSendsToNonContact != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact, - shouldRequireTOTP2FAForSendsToNonContact); - - if (shouldRequireTOTP2FAForSendsToInternalWallets != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets, - shouldRequireTOTP2FAForSendsToInternalWallets); - - if (shouldRequireTOTP2FAForExchangesToInternalWallets != null) - await _sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, - shouldRequireTOTP2FAForExchangesToInternalWallets); - - if (shouldRequireTOTP2FAForAddingContacts != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts, - shouldRequireTOTP2FAForAddingContacts); - - if (shouldRequireTOTP2FAForCreatingNewWallets != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets, - shouldRequireTOTP2FAForCreatingNewWallets); - - if (shouldRequireTOTP2FAForAllSecurityAndBackupSettings != null) - await _sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - shouldRequireTOTP2FAForAllSecurityAndBackupSettings); - if (sortBalanceTokensBy != null) await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); @@ -532,8 +469,6 @@ class BackupService { PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), - PreferencesKey.allowBiometricalAuthenticationKey: - _sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey), PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), PreferencesKey.currentLanguageCode: @@ -550,24 +485,6 @@ class BackupService { _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), PreferencesKey.currentFiatApiModeKey: _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), - PreferencesKey.selectedCake2FAPreset: - _sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset), - PreferencesKey.shouldRequireTOTP2FAForAccessingWallet: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet), - PreferencesKey.shouldRequireTOTP2FAForSendsToContact: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact), - PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact), - PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets), - PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets: _sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets), - PreferencesKey.shouldRequireTOTP2FAForAddingContacts: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts), - PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets), - PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), PreferencesKey.sortBalanceBy: _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), PreferencesKey.pinNativeTokenAtTop: _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), diff --git a/lib/di.dart b/lib/di.dart index 222ac037a..9b474f25c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -277,6 +277,7 @@ Future setup({ if (!_isSetupFinished) { getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); + getIt.registerSingleton(secureStorage); } if (!_isSetupFinished) { getIt.registerFactory(() => BackgroundTasks()); @@ -303,7 +304,6 @@ Future setup({ getIt.registerFactory>(() => _nodeSource); getIt.registerFactory>(() => _powNodeSource, instanceName: Node.boxName + "pow"); - getIt.registerSingleton(secureStorage); getIt.registerSingleton(AuthenticationStore()); getIt.registerSingleton(WalletListStore()); getIt.registerSingleton(NodeListStoreBase.instance); @@ -345,17 +345,19 @@ Future setup({ walletInfoSource: _walletInfoSource)); getIt.registerFactoryParam( - (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get())); + (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get())); getIt.registerFactory(() => WalletLoadingService( getIt.get(), getIt.get(), (WalletType type) => getIt.get(param1: type))); - - getIt.registerFactoryParam((type, _) => - WalletNewVM(getIt.get(), - getIt.get(param1: type), _walletInfoSource, - getIt.get(param1: type),type: type)); + + getIt.registerFactoryParam((type, _) => WalletNewVM( + getIt.get(), + getIt.get(param1: type), + _walletInfoSource, + getIt.get(param1: type), + type: type)); getIt.registerFactoryParam((WalletType type, _) { return WalletRestorationFromQRVM(getIt.get(), @@ -611,7 +613,6 @@ Future setup({ _walletInfoSource, getIt.get(), getIt.get(), - getIt.get(), ), ); } else { @@ -622,7 +623,6 @@ Future setup({ _walletInfoSource, getIt.get(), getIt.get(), - getIt.get(), ), ); } @@ -724,7 +724,7 @@ Future setup({ }); getIt.registerFactory(() { - return SecuritySettingsViewModel(getIt.get(), getIt.get()); + return SecuritySettingsViewModel(getIt.get()); }); getIt.registerFactory(() => WalletSeedViewModel(getIt.get().wallet!)); @@ -806,8 +806,7 @@ Future setup({ .registerFactory(() => DFXBuyProvider(wallet: getIt.get().wallet!)); getIt.registerFactory(() => MoonPaySellProvider( - settingsStore: getIt.get().settingsStore, - wallet: getIt.get().wallet!)); + settingsStore: getIt.get().settingsStore, wallet: getIt.get().wallet!)); getIt.registerFactory(() => OnRamperBuyProvider( getIt.get().settingsStore, @@ -920,8 +919,7 @@ Future setup({ (param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true)); getIt.registerFactoryParam( - (seedPhraseLength, _) - => PreSeedPage(seedPhraseLength)); + (seedPhraseLength, _) => PreSeedPage(seedPhraseLength)); getIt.registerFactoryParam((trade, _) => TradeDetailsViewModel( @@ -955,7 +953,7 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); getIt.registerFactoryParam( - (isBuyOption, _) => BuySellOptionsPage(getIt.get(), isBuyOption)); + (isBuyOption, _) => BuySellOptionsPage(getIt.get(), isBuyOption)); getIt.registerFactory(() { final wallet = getIt.get().wallet; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 22868ae28..1279beeca 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -185,6 +185,9 @@ Future defaultSettingsMigration( case 25: await rewriteSecureStoragePin(secureStorage: secureStorage); break; + case 26: + await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + break; default: break; } @@ -378,6 +381,82 @@ Node getMoneroDefaultNode({required Box nodes}) { } } +Future insecureStorageMigration({ + required SharedPreferences sharedPreferences, + required FlutterSecureStorage secureStorage, +}) async { + bool? allowBiometricalAuthentication = + sharedPreferences.getBool(SecureKey.allowBiometricalAuthenticationKey); + bool? useTOTP2FA = sharedPreferences.getBool(SecureKey.useTOTP2FA); + bool? shouldRequireTOTP2FAForAccessingWallet = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForAccessingWallet); + bool? shouldRequireTOTP2FAForSendsToContact = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForSendsToContact); + bool? shouldRequireTOTP2FAForSendsToNonContact = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForSendsToNonContact); + bool? shouldRequireTOTP2FAForSendsToInternalWallets = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets); + bool? shouldRequireTOTP2FAForExchangesToInternalWallets = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets); + bool? shouldRequireTOTP2FAForExchangesToExternalWallets = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets); + bool? shouldRequireTOTP2FAForAddingContacts = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForAddingContacts); + bool? shouldRequireTOTP2FAForCreatingNewWallets = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForCreatingNewWallets); + bool? shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings); + int? selectedCake2FAPreset = sharedPreferences.getInt(SecureKey.selectedCake2FAPreset); + String? totpSecretKey = sharedPreferences.getString(SecureKey.totpSecretKey); + int? pinTimeOutDuration = sharedPreferences.getInt(SecureKey.pinTimeOutDuration); + int? lastAuthTimeMilliseconds = sharedPreferences.getInt(SecureKey.lastAuthTimeMilliseconds); + + try { + await secureStorage.write( + key: SecureKey.allowBiometricalAuthenticationKey, + value: allowBiometricalAuthentication.toString()); + await secureStorage.write(key: SecureKey.useTOTP2FA, value: useTOTP2FA.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAccessingWallet, + value: shouldRequireTOTP2FAForAccessingWallet.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToContact, + value: shouldRequireTOTP2FAForSendsToContact.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToNonContact, + value: shouldRequireTOTP2FAForSendsToNonContact.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets, + value: shouldRequireTOTP2FAForSendsToInternalWallets.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + value: shouldRequireTOTP2FAForExchangesToInternalWallets.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + value: shouldRequireTOTP2FAForExchangesToExternalWallets.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAddingContacts, + value: shouldRequireTOTP2FAForAddingContacts.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForCreatingNewWallets, + value: shouldRequireTOTP2FAForCreatingNewWallets.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + value: shouldRequireTOTP2FAForAllSecurityAndBackupSettings.toString()); + await secureStorage.write( + key: SecureKey.selectedCake2FAPreset, value: selectedCake2FAPreset.toString()); + await secureStorage.write(key: SecureKey.totpSecretKey, value: totpSecretKey.toString()); + await secureStorage.write( + key: SecureKey.pinTimeOutDuration, value: pinTimeOutDuration.toString()); + await secureStorage.write( + key: SecureKey.lastAuthTimeMilliseconds, value: lastAuthTimeMilliseconds.toString()); + } catch (e) { + print("Error migrating shared preferences to secure storage!: $e"); + // this actually shouldn't be that big of a problem since we don't delete the old keys in this update + // and we read and write to the new locations when loading storage, the migration is just for extra safety + } +} + Future rewriteSecureStoragePin({required FlutterSecureStorage secureStorage}) async { // the bug only affects ios/mac: if (!Platform.isIOS && !Platform.isMacOS) { diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index e184f5649..52bcc495b 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -6,12 +6,14 @@ import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/unstoppable_domain_address.dart'; import 'package:cake_wallet/entities/emoji_string_extension.dart'; import 'package:cake_wallet/mastodon/mastodon_api.dart'; +import 'package:cake_wallet/nostr/nostr_api.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/twitter/twitter_api.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/entities/fio_address_provider.dart'; +import 'package:flutter/cupertino.dart'; class AddressResolver { AddressResolver({required this.yatService, required this.wallet, required this.settingsStore}) @@ -58,7 +60,16 @@ class AddressResolver { }); } - Future resolve(String text, String ticker) async { + bool isEmailFormat(String address) { + final RegExp emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + caseSensitive: false, + ); + return emailRegex.hasMatch(address); + } + + + Future resolve(BuildContext context, String text, String ticker) async { try { if (text.startsWith('@') && !text.substring(1).contains('@')) { if(settingsStore.lookupsTwitter) { @@ -165,6 +176,21 @@ class AddressResolver { } } } + if (isEmailFormat(text)) { + final nostrProfile = await NostrProfileHandler.queryProfile(context, text); + if (nostrProfile?.relays != null) { + final nostrUserData = + await NostrProfileHandler.processRelays(context, nostrProfile!, text); + + if (nostrUserData != null) { + String? addressFromBio = extractAddressByType( + raw: nostrUserData.about, type: CryptoCurrency.fromString(ticker)); + if (addressFromBio != null) { + return ParsedAddress.nostrAddress(address: addressFromBio, name: text); + } + } + } + } } catch (e) { print(e.toString()); } diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index df20dd9ee..d414a827d 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -2,7 +2,18 @@ import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/yat_record.dart'; -enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, ens, contact, mastodon } +enum ParseFrom { + unstoppableDomains, + openAlias, + yatRecord, + fio, + notParsed, + twitter, + ens, + contact, + mastodon, + nostr +} class ParsedAddress { ParsedAddress({ @@ -11,9 +22,9 @@ class ParsedAddress { this.description = '', this.parseFrom = ParseFrom.notParsed, }); - + factory ParsedAddress.fetchEmojiAddress({ - List? addresses, + List? addresses, required String name, }){ if (addresses?.isEmpty ?? true) { @@ -28,7 +39,7 @@ class ParsedAddress { } factory ParsedAddress.fetchUnstoppableDomainAddress({ - String? address, + String? address, required String name, }){ if (address?.isEmpty ?? true) { @@ -94,6 +105,14 @@ class ParsedAddress { ); } + factory ParsedAddress.nostrAddress({required String address, required String name}) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.nostr, + ); + } + final List addresses; final String name; final String description; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 13b2fe8ae..6df223f2e 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -23,8 +23,6 @@ class PreferencesKey { static const walletListOrder = 'wallet_list_order'; static const walletListAscending = 'wallet_list_ascending'; static const currentFiatApiModeKey = 'current_fiat_api_mode'; - static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication'; - static const useTOTP2FA = 'use_totp_2fa'; static const failedTotpTokenTrials = 'failed_token_trials'; static const disableExchangeKey = 'disable_exchange'; static const exchangeStatusKey = 'exchange_status'; @@ -33,6 +31,7 @@ class PreferencesKey { static const displayActionListModeKey = 'display_list_mode'; static const currentPinLength = 'current_pin_length'; static const currentLanguageCode = 'language_code'; + static const currentSeedPhraseLength = 'current_seed_phrase_length'; static const currentDefaultSettingsMigrationVersion = 'current_default_settings_migration_version'; static const moneroTransactionPriority = 'current_fee_priority_monero'; @@ -47,8 +46,6 @@ class PreferencesKey { static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; static const syncModeKey = 'sync_mode'; static const syncAllKey = 'sync_all'; - static const pinTimeOutDuration = 'pin_timeout_duration'; - static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastPopupDate = 'last_popup_date'; static const lastAppReviewDate = 'last_app_review_date'; static const sortBalanceBy = 'sort_balance_by'; @@ -75,26 +72,5 @@ class PreferencesKey { static const lastSeenAppVersion = 'last_seen_app_version'; static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; static const isNewInstall = 'is_new_install'; - static const shouldRequireTOTP2FAForAccessingWallet = - 'should_require_totp_2fa_for_accessing_wallets'; - static const shouldRequireTOTP2FAForSendsToContact = - 'should_require_totp_2fa_for_sends_to_contact'; - static const shouldRequireTOTP2FAForSendsToNonContact = - 'should_require_totp_2fa_for_sends_to_non_contact'; - static const shouldRequireTOTP2FAForSendsToInternalWallets = - 'should_require_totp_2fa_for_sends_to_internal_wallets'; - static const shouldRequireTOTP2FAForExchangesToInternalWallets = - 'should_require_totp_2fa_for_exchanges_to_internal_wallets'; - static const shouldRequireTOTP2FAForExchangesToExternalWallets = - 'should_require_totp_2fa_for_exchanges_to_external_wallets'; - static const shouldRequireTOTP2FAForAddingContacts = - 'should_require_totp_2fa_for_adding_contacts'; - static const shouldRequireTOTP2FAForCreatingNewWallets = - 'should_require_totp_2fa_for_creating_new_wallets'; - static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings = - 'should_require_totp_2fa_for_all_security_and_backup_settings'; - static const selectedCake2FAPreset = 'selected_cake_2fa_preset'; - static const totpSecretKey = 'totp_secret_key'; - static const currentSeedPhraseLength = 'current_seed_phrase_length'; static const thorChainTradeCounter = 'thor_chain_trade_counter'; } diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index a3c203960..ed688590c 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -65,9 +65,10 @@ class ProvidersHelper { case WalletType.litecoin: case WalletType.bitcoinCash: return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; + case WalletType.polygon: + return [ProviderType.askEachTime, ProviderType.dfx]; case WalletType.none: case WalletType.haven: - case WalletType.polygon: return []; } } @@ -76,17 +77,22 @@ class ProvidersHelper { switch (walletType) { case WalletType.bitcoin: case WalletType.ethereum: - return [ProviderType.askEachTime, ProviderType.onramper, - ProviderType.moonpaySell, ProviderType.dfx]; + return [ + ProviderType.askEachTime, + ProviderType.onramper, + ProviderType.moonpaySell, + ProviderType.dfx, + ]; case WalletType.litecoin: case WalletType.bitcoinCash: return [ProviderType.askEachTime, ProviderType.moonpaySell]; + case WalletType.polygon: + return [ProviderType.askEachTime, ProviderType.dfx]; case WalletType.monero: case WalletType.nano: case WalletType.banano: case WalletType.none: case WalletType.haven: - case WalletType.polygon: return []; } } diff --git a/lib/entities/secret_store_key.dart b/lib/entities/secret_store_key.dart index 00ec06767..2ee490c74 100644 --- a/lib/entities/secret_store_key.dart +++ b/lib/entities/secret_store_key.dart @@ -1,3 +1,6 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + enum SecretStoreKey { moneroWalletPassword, pinCodePassword, backupPassword } const moneroWalletPassword = "MONERO_WALLET_PASSWORD"; @@ -35,3 +38,65 @@ String generateStoreKeyFor({ return _key; } + +class SecureKey { + static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication'; + static const useTOTP2FA = 'use_totp_2fa'; + static const shouldRequireTOTP2FAForAccessingWallet = + 'should_require_totp_2fa_for_accessing_wallets'; + static const shouldRequireTOTP2FAForSendsToContact = + 'should_require_totp_2fa_for_sends_to_contact'; + static const shouldRequireTOTP2FAForSendsToNonContact = + 'should_require_totp_2fa_for_sends_to_non_contact'; + static const shouldRequireTOTP2FAForSendsToInternalWallets = + 'should_require_totp_2fa_for_sends_to_internal_wallets'; + static const shouldRequireTOTP2FAForExchangesToInternalWallets = + 'should_require_totp_2fa_for_exchanges_to_internal_wallets'; + static const shouldRequireTOTP2FAForExchangesToExternalWallets = + 'should_require_totp_2fa_for_exchanges_to_external_wallets'; + static const shouldRequireTOTP2FAForAddingContacts = + 'should_require_totp_2fa_for_adding_contacts'; + static const shouldRequireTOTP2FAForCreatingNewWallets = + 'should_require_totp_2fa_for_creating_new_wallets'; + static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + 'should_require_totp_2fa_for_all_security_and_backup_settings'; + static const selectedCake2FAPreset = 'selected_cake_2fa_preset'; + static const totpSecretKey = 'totp_secret_key'; + static const pinTimeOutDuration = 'pin_timeout_duration'; + static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; + + static Future getInt({ + required FlutterSecureStorage secureStorage, + required SharedPreferences sharedPreferences, + required String key, + }) async { + int? value = int.tryParse((await secureStorage.read(key: key) ?? '')); + value ??= sharedPreferences.getInt(key); + return value; + } + + static Future getBool({ + required FlutterSecureStorage secureStorage, + required SharedPreferences sharedPreferences, + required String key, + }) async { + String? value = (await secureStorage.read(key: key) ?? ''); + if (value.toLowerCase() == "true") { + return true; + } else if (value.toLowerCase() == "false") { + return false; + } else { + return sharedPreferences.getBool(key); + } + } + + static Future getString({ + required FlutterSecureStorage secureStorage, + required SharedPreferences sharedPreferences, + required String key, + }) async { + String? value = await secureStorage.read(key: key); + value ??= sharedPreferences.getString(key); + return value; + } +} diff --git a/lib/main.dart b/lib/main.dart index 165db1ddd..306b109a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -163,7 +163,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 25); + initialMigrationVersion: 26); } Future initialSetup( diff --git a/lib/nostr/nostr_api.dart b/lib/nostr/nostr_api.dart new file mode 100644 index 000000000..7c0eea5ef --- /dev/null +++ b/lib/nostr/nostr_api.dart @@ -0,0 +1,142 @@ +import 'dart:convert'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/nostr/nostr_user.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:flutter/material.dart'; +import 'package:nostr_tools/nostr_tools.dart'; + +class NostrProfileHandler { + static final relayToDomainMap = { + 'relay.snort.social': 'snort.social', + }; + + static Nip05 _nip05 = Nip05(); + + static Future queryProfile(BuildContext context, String nip05Address) async { + var profile = await _nip05.queryProfile(nip05Address); + if (profile?.pubkey != null) { + if (profile?.relays?.isNotEmpty == true) { + return profile; + } else { + await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relays_message); + } + } + return null; + } + + static Future processRelays( + BuildContext context, ProfilePointer profile, String nip05Address) async { + String userDomain = _extractDomain(nip05Address); + const int metaData = 0; + + for (String relayUrl in profile.relays ?? []) { + final relayDomain = _getDomainFromRelayUrl(relayUrl); + final formattedRelayDomain = relayToDomainMap[relayDomain] ?? relayDomain; + if (formattedRelayDomain == userDomain) { + final userDomainData = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaData]); + if (userDomainData != null) { + return userDomainData; + } + } + } + await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relay_on_domain); + + String? chosenRelayUrl = await _showRelayChoiceDialog(context, profile.relays ?? []); + if (chosenRelayUrl != null) { + final userData = await _fetchInfoFromRelay(chosenRelayUrl, profile.pubkey, [metaData]); + if (userData != null) { + return userData; + } + } + + return null; + } + + static Future _fetchInfoFromRelay( + String relayUrl, String userPubKey, List kinds) async { + try { + final relay = RelayApi(relayUrl: relayUrl); + final stream = await relay.connect(); + + relay.sub([ + Filter( + kinds: kinds, + authors: [userPubKey], + ) + ]); + + await for (var message in stream) { + if (message.type == 'EVENT') { + final event = message.message as Event; + + final eventContent = json.decode(event.content) as Map; + + final userMetadata = UserMetadata.fromJson(eventContent); + relay.close(); + return userMetadata; + } + } + + relay.close(); + return null; + } catch (e) { + print('[!] Error with relay $relayUrl: $e'); + return null; + } + } + + static Future _showErrorDialog( + BuildContext context, String title, String errorMessage) async { + if (context.mounted) { + await showPopUp( + context: context, + builder: (BuildContext dialogContext) { + return AlertWithOneAction( + alertTitle: title, + alertContent: errorMessage, + buttonText: S.of(dialogContext).ok, + buttonAction: () => Navigator.of(dialogContext).pop(), + ); + }, + ); + } + } + + static String _extractDomain(String nip05Address) { + var parts = nip05Address.split('@'); + return parts.length == 2 ? parts[1] : ''; + } + + static String _getDomainFromRelayUrl(String relayUrl) { + try { + var uri = Uri.parse(relayUrl); + return uri.host; + } catch (e) { + print('Error parsing URL: $e'); + return ''; + } + } + + static Future _showRelayChoiceDialog(BuildContext context, List relays) async { + String? selectedRelay; + + if (context.mounted) { + await showPopUp( + context: context, + builder: (BuildContext dialogContext) { + return Picker( + selectedAtIndex: 0, + title: S.of(dialogContext).choose_relay, + items: relays, + onItemSelected: (String relay) => selectedRelay = relay, + ); + }, + ); + } + + return selectedRelay; + } +} diff --git a/lib/nostr/nostr_user.dart b/lib/nostr/nostr_user.dart new file mode 100644 index 000000000..4b1694a11 --- /dev/null +++ b/lib/nostr/nostr_user.dart @@ -0,0 +1,35 @@ + +class UserMetadata { + final String name; + final String lnurl; + final String email; + final String picture; + final String about; + final String nip05; + final String banner; + final String website; + + UserMetadata({ + required this.name, + required this.lnurl, + required this.email, + required this.picture, + required this.about, + required this.nip05, + required this.banner, + required this.website, + }); + + factory UserMetadata.fromJson(Map json) { + return UserMetadata( + name: json['name'] as String? ?? '', + lnurl: json['lud06'] as String? ?? '', + email: json['lud16'] as String? ?? '', + picture: json['picture'] as String? ?? '', + about: json['about'] as String? ?? '', + nip05: json['nip05'] as String? ?? '', + banner: json['banner'] as String? ?? '', + website: json['website'] as String? ?? '', + ); + } +} \ No newline at end of file diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 42fbd182e..ade9927ff 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -68,7 +68,8 @@ void startCurrentWalletChangeReaction( .get() .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); - if (wallet.type == WalletType.monero) { + if (wallet.type == WalletType.monero || wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash ) { _setAutoGenerateSubaddressStatus(wallet, settingsStore); } diff --git a/lib/src/screens/buy/webview_page.dart b/lib/src/screens/buy/webview_page.dart index 97387c29a..0af5952c4 100644 --- a/lib/src/screens/buy/webview_page.dart +++ b/lib/src/screens/buy/webview_page.dart @@ -1,4 +1,7 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -14,13 +17,14 @@ class WebViewPage extends BasePage { @override Widget body(BuildContext context) { - return WebViewPageBody(_url); + return WebViewPageBody(_title, _url); } } class WebViewPageBody extends StatefulWidget { - WebViewPageBody(this.uri); + WebViewPageBody(this.title, this.uri); + final String title; final Uri uri; @override @@ -40,6 +44,27 @@ class WebViewPageBodyState extends State { onPermissionRequest: (controller, request) async { bool permissionGranted = await Permission.camera.status == PermissionStatus.granted; if (!permissionGranted) { + final bool userConsent = await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).privacy, + alertContent: S.of(context).camera_consent(widget.title), + rightButtonText: S.of(context).agree, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; + + /// if user did NOT give the consent then return permission denied + if (!userConsent) { + return PermissionResponse( + resources: request.resources, + action: PermissionResponseAction.DENY, + ); + } + permissionGranted = await Permission.camera.request().isGranted; } diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index f3dd2427d..a81a3f6e4 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -166,12 +166,16 @@ class _DesktopWalletSelectionDropDownState extends State dashboardViewModel.isAutoGenerateSubaddressesEnabled + return SelectButton( + text: addressListViewModel.buttonTitle, + onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled && + (WalletType.monero == addressListViewModel.wallet.type || + WalletType.haven == addressListViewModel.wallet.type) ? await showPopUp( - context: context, builder: (_) => getIt.get()) + context: context, + builder: (_) => getIt.get()) : Navigator.of(context).pushNamed(Routes.receive), - child: Container( - height: 50, - padding: EdgeInsets.only(left: 24, right: 12), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(25)), - border: Border.all( - color: - Theme.of(context).extension()!.cardBorderColor, - width: 1), - color: Theme.of(context) - .extension()! - .syncedBackgroundColor), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Observer( - builder: (_) { - String label = addressListViewModel.hasAccounts - ? S.of(context).accounts_subaddresses - : S.of(context).addresses; - - if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) { - label = addressListViewModel.hasAccounts - ? S.of(context).accounts - : S.of(context).account; - } - return Text( - label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .extension()! - .textColor), - ); - }, - ), - Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).extension()!.textColor, - ) - ], - ), - ), + textColor: Theme.of(context).extension()!.textColor, + color: Theme.of(context).extension()!.syncedBackgroundColor, + borderColor: Theme.of(context).extension()!.cardBorderColor, + arrowColor: Theme.of(context).extension()!.textColor, + textSize: 14, + height: 50, ); } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled || - addressListViewModel.showElectrumAddressDisclaimer) { + addressListViewModel.isElectrumWallet) { return Text(S.of(context).electrum_address_disclaimer, textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 76b1596ac..6715a62e7 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -560,7 +560,7 @@ class ExchangePage extends BasePage { } Future fetchParsedAddress(BuildContext context, String domain, String ticker) async { - final parsedAddress = await getIt.get().resolve(domain, ticker); + final parsedAddress = await getIt.get().resolve(context, domain, ticker); final address = await extractAddressFromParsed(context, parsedAddress); return address; } diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 729f20ead..d921d6278 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'dart:ui'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; @@ -344,7 +345,11 @@ class ExchangeTradeState extends State { bottom: 24, child: PrimaryButton( onPressed: () { - Navigator.of(popupContext).pop(); + Navigator.pushNamedAndRemoveUntil( + popupContext, + Routes.dashboard, + (route) => false, + ); RequestReviewHandler.requestReview(); }, text: S.of(popupContext).got_it, diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 225e5b82d..8c8a94a7e 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -1,12 +1,14 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; +import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -26,15 +28,18 @@ class NewWalletTypePage extends BasePage { @override Widget body(BuildContext context) => WalletTypeForm( - onTypeSelected: onTypeSelected, - walletImage: currentTheme.type == ThemeType.dark ? walletTypeImage : walletTypeLightImage); + onTypeSelected: onTypeSelected, + walletImage: currentTheme.type == ThemeType.dark ? walletTypeImage : walletTypeLightImage, + isCreate: isCreate, + ); } class WalletTypeForm extends StatefulWidget { - WalletTypeForm({required this.onTypeSelected, required this.walletImage}); + WalletTypeForm({required this.onTypeSelected, required this.walletImage, required this.isCreate}); final void Function(BuildContext, WalletType) onTypeSelected; final Image walletImage; + final bool isCreate; @override WalletTypeFormState createState() => WalletTypeFormState(); @@ -70,7 +75,8 @@ class WalletTypeFormState extends State { Widget build(BuildContext context) { return Center( child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( children: [ Padding( @@ -128,6 +134,19 @@ class WalletTypeFormState extends State { throw Exception('Wallet Type is not selected yet.'); } + if (selected == WalletType.haven && widget.isCreate) { + return await showPopUp( + context: context, + builder: (BuildContext context) { + return PopUpCancellableAlertDialog( + contentText: S.of(context).pause_wallet_creation, + actionButtonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } + widget.onTypeSelected(context, selected!); } } diff --git a/lib/src/screens/new_wallet/widgets/select_button.dart b/lib/src/screens/new_wallet/widgets/select_button.dart index e220b281e..d94c9767d 100644 --- a/lib/src/screens/new_wallet/widgets/select_button.dart +++ b/lib/src/screens/new_wallet/widgets/select_button.dart @@ -11,29 +11,37 @@ class SelectButton extends StatelessWidget { this.isSelected = false, this.showTrailingIcon = true, this.height = 60, + this.textSize = 18, + this.color, + this.textColor, + this.arrowColor, + this.borderColor, }); final Image? image; final String text; + final double textSize; final bool isSelected; final VoidCallback onTap; final bool showTrailingIcon; final double height; + final Color? color; + final Color? textColor; + final Color? arrowColor; + final Color? borderColor; @override Widget build(BuildContext context) { - final color = isSelected - ? Colors.green - : Theme.of(context).cardColor; - final textColor = isSelected + final backgroundColor = color ?? (isSelected ? Colors.green : Theme.of(context).cardColor); + final effectiveTextColor = textColor ?? (isSelected ? Theme.of(context).extension()!.restoreWalletButtonTextColor - : Theme.of(context).extension()!.buttonTextColor; - final arrowColor = isSelected + : Theme.of(context).extension()!.buttonTextColor); + final effectiveArrowColor = arrowColor ?? (isSelected ? Theme.of(context).extension()!.restoreWalletButtonTextColor - : Theme.of(context).extension()!.titlesColor; + : Theme.of(context).extension()!.titlesColor); final selectArrowImage = Image.asset('assets/images/select_arrow.png', - color: arrowColor); + color: effectiveArrowColor); return GestureDetector( onTap: onTap, @@ -44,7 +52,9 @@ class SelectButton extends StatelessWidget { alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(30)), - color: color + color: backgroundColor, + border: borderColor != null ? Border.all(color: borderColor!) : null, + ), child: Row( mainAxisSize: MainAxisSize.max, @@ -63,9 +73,9 @@ class SelectButton extends StatelessWidget { child: Text( text, style: TextStyle( - fontSize: 18, + fontSize: textSize, fontWeight: FontWeight.w500, - color: textColor + color: effectiveTextColor, ), ), ) diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 87b668e34..75719d123 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -49,7 +49,7 @@ class ReceivePage extends BasePage { bool get gradientBackground => true; @override - bool get resizeToAvoidBottomInset => false; + bool get resizeToAvoidBottomInset => true; final FocusNode _cryptoAmountFocus; @@ -99,10 +99,11 @@ class ReceivePage extends BasePage { @override Widget body(BuildContext context) { + final isElectrumWallet = addressListViewModel.isElectrumWallet; return (addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven || addressListViewModel.type == WalletType.nano || - addressListViewModel.type == WalletType.banano) + isElectrumWallet) ? KeyboardActions( config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, @@ -140,7 +141,9 @@ class ReceivePage extends BasePage { if (item is WalletAccountListHeader) { cell = HeaderTile( - onTap: () async { + showTrailingButton: true, + walletAddressListViewModel: addressListViewModel, + trailingButtonTap: () async { if (addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven) { await showPopUp( @@ -153,7 +156,7 @@ class ReceivePage extends BasePage { } }, title: S.of(context).accounts, - icon: Icon( + trailingIcon: Icon( Icons.arrow_forward_ios, size: 14, color: Theme.of(context).extension()!.iconsColor, @@ -161,16 +164,21 @@ class ReceivePage extends BasePage { } if (item is WalletAddressListHeader) { - cell = HeaderTile( - onTap: () => - Navigator.of(context).pushNamed(Routes.newSubaddress), - title: S.of(context).addresses, - icon: Icon( - Icons.add, - size: 20, - color: Theme.of(context).extension()!.iconsColor, - )); - } + cell = HeaderTile( + title: S.of(context).addresses, + walletAddressListViewModel: addressListViewModel, + showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled, + showSearchButton: true, + trailingButtonTap: () => + Navigator.of(context).pushNamed(Routes.newSubaddress), + trailingIcon: Icon( + Icons.add, + size: 20, + color: Theme.of(context) + .extension()! + .iconsColor, + )); + } if (item is WalletAddressListItem) { cell = Observer(builder: (_) { @@ -185,6 +193,7 @@ class ReceivePage extends BasePage { return AddressCell.fromItem(item, isCurrent: isCurrent, + hasBalance: addressListViewModel.isElectrumWallet, backgroundColor: backgroundColor, textColor: textColor, onTap: (_) => addressListViewModel.setAddress(item), diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 733612e0b..92c870421 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -1,7 +1,8 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; class AddressCell extends StatelessWidget { AddressCell( @@ -12,13 +13,18 @@ class AddressCell extends StatelessWidget { required this.backgroundColor, required this.textColor, this.onTap, - this.onEdit}); + this.onEdit, + this.txCount, + this.balance, + this.isChange = false, + this.hasBalance = false}); factory AddressCell.fromItem(WalletAddressListItem item, {required bool isCurrent, required Color backgroundColor, required Color textColor, Function(String)? onTap, + bool hasBalance = false, Function()? onEdit}) => AddressCell( address: item.address, @@ -28,7 +34,11 @@ class AddressCell extends StatelessWidget { backgroundColor: backgroundColor, textColor: textColor, onTap: onTap, - onEdit: onEdit); + onEdit: onEdit, + txCount: item.txCount, + balance: item.balance, + isChange: item.isChange, + hasBalance: hasBalance); final String address; final String name; @@ -38,17 +48,22 @@ class AddressCell extends StatelessWidget { final Color textColor; final Function(String)? onTap; final Function()? onEdit; + final int? txCount; + final String? balance; + final bool isChange; + final bool hasBalance; - String get label { - if (name.isEmpty){ - if(address.length<=16){ - return address; - }else{ - return address.substring(0,8)+'...'+ - address.substring(address.length-8,address.length); - } - }else{ - return name; + static const int addressPreviewLength = 8; + + String get formattedAddress { + final formatIfCashAddr = address.replaceAll('bitcoincash:', ''); + + if (formatIfCashAddr.length <= (name.isNotEmpty ? 16 : 43)) { + return formatIfCashAddr; + } else { + return formatIfCashAddr.substring(0, addressPreviewLength) + + '...' + + formatIfCashAddr.substring(formatIfCashAddr.length - addressPreviewLength, formatIfCashAddr.length); } } @@ -59,41 +74,114 @@ class AddressCell extends StatelessWidget { child: Container( width: double.infinity, color: backgroundColor, - padding: EdgeInsets.only(left: 24, right: 24, top: 28, bottom: 28), - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - color: textColor, - ), + padding: EdgeInsets.only(left: 24, right: 24, top: 20, bottom: 20), + child: Row( + children: [ + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + if (isChange) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Container( + height: 20, + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: textColor), + alignment: Alignment.center, + child: Text( + S.of(context).unspent_change, + style: TextStyle( + color: backgroundColor, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + if (name.isNotEmpty) + Text( + '$name - ', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + AutoSizeText( + formattedAddress, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isChange ? 10 : 14, + color: textColor, + ), + ), + ], + ), + if (hasBalance) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + 'Balance: $balance', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + Text( + '${S.of(context).transactions.toLowerCase()}: $txCount', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ], + ), + ), + ], + ), + ), + ], ), )); - return Semantics( - label: S.of(context).slidable, - selected: isCurrent, - enabled: !isCurrent, - child: Slidable( - key: Key(address), - startActionPane: _actionPane(context), - endActionPane: _actionPane(context), - child: cell, - ), - ); + return onEdit == null + ? cell + : Semantics( + label: S.of(context).slidable, + selected: isCurrent, + enabled: !isCurrent, + child: Slidable( + key: Key(address), + startActionPane: _actionPane(context), + endActionPane: _actionPane(context), + child: cell, + ), + ); } ActionPane _actionPane(BuildContext context) => ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.3, - children: [ - SlidableAction( - onPressed: (_) => onEdit?.call(), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - icon: Icons.edit, - label: S.of(context).edit, - ), - ], - ); + motion: const ScrollMotion(), + extentRatio: 0.3, + children: [ + SlidableAction( + onPressed: (_) => onEdit?.call(), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: Icons.edit, + label: S.of(context).edit, + ), + ], + ); } diff --git a/lib/src/screens/receive/widgets/header_tile.dart b/lib/src/screens/receive/widgets/header_tile.dart index e9c134ea5..faaa9ed07 100644 --- a/lib/src/screens/receive/widgets/header_tile.dart +++ b/lib/src/screens/receive/widgets/header_tile.dart @@ -1,50 +1,114 @@ -import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:flutter/material.dart'; -class HeaderTile extends StatelessWidget { +class HeaderTile extends StatefulWidget { HeaderTile({ - required this.onTap, required this.title, - required this.icon + required this.walletAddressListViewModel, + this.showSearchButton = false, + this.showTrailingButton = false, + this.trailingButtonTap, + this.trailingIcon, }); - final VoidCallback onTap; final String title; - final Icon icon; + final WalletAddressListViewModel walletAddressListViewModel; + final bool showSearchButton; + final bool showTrailingButton; + final VoidCallback? trailingButtonTap; + final Icon? trailingIcon; + + @override + _HeaderTileState createState() => _HeaderTileState(); +} + +class _HeaderTileState extends State { + bool _isSearchActive = false; @override Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: EdgeInsets.only( - left: 24, - right: 24, - top: 24, - bottom: 24 - ), - color: Theme.of(context).extension()!.tilesBackgroundColor, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Theme.of(context).extension()!.tilesTextColor), - ), - Container( - height: 32, - width: 32, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).extension()!.iconsBackgroundColor), - child: icon, - ) - ], - ), + final searchIcon = Image.asset("assets/images/search_icon.png", + color: Theme.of(context).extension()!.iconsColor); + + return Container( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + color: Theme.of(context).extension()!.tilesBackgroundColor, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _isSearchActive + ? Expanded( + child: TextField( + onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value), + cursorColor: Theme.of(context).extension()!.tilesTextColor, + cursorWidth: 0.5, + decoration: InputDecoration( + hintText: '${S.of(context).search}...', + isDense: true, + contentPadding: EdgeInsets.zero, + hintStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.tilesTextColor), + border: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).dividerColor), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).dividerColor), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + autofocus: true, + ), + ) + : Text( + widget.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.tilesTextColor), + ), + Row( + children: [ + if (widget.showSearchButton) + GestureDetector( + onTap: () { + setState(() { + _isSearchActive = !_isSearchActive; + widget.walletAddressListViewModel.updateSearchText(''); + }); + }, + child: Container( + height: 32, + width: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context) + .extension()! + .iconsBackgroundColor), + child: searchIcon, + )), + const SizedBox(width: 8), + if (widget.showTrailingButton) + GestureDetector( + onTap: widget.trailingButtonTap, + child: Container( + height: 32, + width: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + Theme.of(context).extension()!.iconsBackgroundColor), + child: widget.trailingIcon, + ), + ), + ], + ), + ], ), ); } diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index bbfd4d5c1..bedb3b526 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/themes/extensions/qr_code_theme.dart'; import 'package:cake_wallet/routes.dart'; @@ -6,6 +7,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dar import 'package:cake_wallet/utils/brightness_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -144,9 +146,10 @@ class QRWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Text( + child: AutoSizeText( addressListViewModel.address.address, textAlign: TextAlign.center, + maxLines: addressListViewModel.wallet.type == WalletType.monero ? 2 : 1, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 5f7fcd205..b3d503162 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -53,13 +53,17 @@ class RootState extends State with WidgetsBindingObserver { @override void initState() { - _requestAuth = widget.authService.requireAuth(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + bool value = await widget.authService.requireAuth(); + setState(() { + _requestAuth = value; + }); + }); _isInactiveController = StreamController.broadcast(); _isInactive = false; _postFrameCallback = false; WidgetsBinding.instance.addObserver(this); super.initState(); - if (DeviceInfo.instance.isMobile) { initUniLinks(); } @@ -105,8 +109,10 @@ class RootState extends State with WidgetsBindingObserver { break; case AppLifecycleState.resumed: - setState(() { - _requestAuth = widget.authService.requireAuth(); + widget.authService.requireAuth().then((value) { + setState(() { + _requestAuth = value; + }); }); break; default: diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index 73bff23c1..bb09d4ca3 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -43,6 +43,11 @@ Future extractAddressFromParsed( content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)'); address = parsedAddress.addresses.first; break; + case ParseFrom.nostr: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (Nostr NIP-05)'); + address = parsedAddress.addresses.first; + break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { title = S.of(context).yat_error; diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 65069e903..07fb34cb2 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -478,7 +478,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin.of(nodes), powNodes = ObservableMap.of(powNodes), + _secureStorage = secureStorage, _sharedPreferences = sharedPreferences, _backgroundTasks = backgroundTasks, fiatCurrency = initialFiatCurrency, @@ -187,8 +191,9 @@ abstract class SettingsStoreBase with Store { final key = 'buyProvider_${walletType.toString()}'; final providerId = sharedPreferences.getString(key); if (providerId != null) { - defaultBuyProviders[walletType] = ProviderType.values - .firstWhere((provider) => provider.id == providerId, orElse: () => ProviderType.askEachTime); + defaultBuyProviders[walletType] = ProviderType.values.firstWhere( + (provider) => provider.id == providerId, + orElse: () => ProviderType.askEachTime); } else { defaultBuyProviders[walletType] = ProviderType.askEachTime; } @@ -198,8 +203,9 @@ abstract class SettingsStoreBase with Store { final key = 'sellProvider_${walletType.toString()}'; final providerId = sharedPreferences.getString(key); if (providerId != null) { - defaultSellProviders[walletType] = ProviderType.values - .firstWhere((provider) => provider.id == providerId, orElse: () => ProviderType.askEachTime); + defaultSellProviders[walletType] = ProviderType.values.firstWhere( + (provider) => provider.id == providerId, + orElse: () => ProviderType.askEachTime); } else { defaultSellProviders[walletType] = ProviderType.askEachTime; } @@ -312,74 +318,6 @@ abstract class SettingsStoreBase with Store { reaction((_) => currentTheme, (ThemeBase theme) => sharedPreferences.setInt(PreferencesKey.currentTheme, theme.raw)); - reaction( - (_) => allowBiometricalAuthentication, - (bool biometricalAuthentication) => sharedPreferences.setBool( - PreferencesKey.allowBiometricalAuthenticationKey, biometricalAuthentication)); - - reaction( - (_) => selectedCake2FAPreset, - (Cake2FAPresetsOptions selectedCake2FAPreset) => sharedPreferences.setInt( - PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset.serialize())); - - reaction( - (_) => shouldRequireTOTP2FAForAccessingWallet, - (bool requireTOTP2FAForAccessingWallet) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForAccessingWallet, - requireTOTP2FAForAccessingWallet)); - - reaction( - (_) => shouldRequireTOTP2FAForSendsToContact, - (bool requireTOTP2FAForSendsToContact) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForSendsToContact, requireTOTP2FAForSendsToContact)); - - reaction( - (_) => shouldRequireTOTP2FAForSendsToNonContact, - (bool requireTOTP2FAForSendsToNonContact) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact, - requireTOTP2FAForSendsToNonContact)); - - reaction( - (_) => shouldRequireTOTP2FAForSendsToInternalWallets, - (bool requireTOTP2FAForSendsToInternalWallets) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets, - requireTOTP2FAForSendsToInternalWallets)); - - reaction( - (_) => shouldRequireTOTP2FAForExchangesToInternalWallets, - (bool requireTOTP2FAForExchangesToInternalWallets) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, - requireTOTP2FAForExchangesToInternalWallets)); - - reaction( - (_) => shouldRequireTOTP2FAForExchangesToExternalWallets, - (bool requireTOTP2FAForExchangesToExternalWallets) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForExchangesToExternalWallets, - requireTOTP2FAForExchangesToExternalWallets)); - - reaction( - (_) => shouldRequireTOTP2FAForAddingContacts, - (bool requireTOTP2FAForAddingContacts) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForAddingContacts, requireTOTP2FAForAddingContacts)); - - reaction( - (_) => shouldRequireTOTP2FAForCreatingNewWallets, - (bool requireTOTP2FAForCreatingNewWallets) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets, - requireTOTP2FAForCreatingNewWallets)); - - reaction( - (_) => shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - (bool requireTOTP2FAForAllSecurityAndBackupSettings) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - requireTOTP2FAForAllSecurityAndBackupSettings)); - - reaction( - (_) => useTOTP2FA, (bool use) => sharedPreferences.setBool(PreferencesKey.useTOTP2FA, use)); - - reaction((_) => totpSecretKey, - (String totpKey) => sharedPreferences.setString(PreferencesKey.totpSecretKey, totpKey)); - reaction( (_) => numberOfFailedTokenTrials, (int failedTokenTrail) => @@ -403,11 +341,6 @@ abstract class SettingsStoreBase with Store { (SeedPhraseLength seedPhraseWordCount) => sharedPreferences.setInt( PreferencesKey.currentSeedPhraseLength, seedPhraseWordCount.value)); - reaction( - (_) => pinTimeOutDuration, - (PinCodeRequiredDuration pinCodeInterval) => - sharedPreferences.setInt(PreferencesKey.pinTimeOutDuration, pinCodeInterval.value)); - reaction( (_) => balanceDisplayMode, (BalanceDisplayMode mode) => sharedPreferences.setInt( @@ -485,6 +418,84 @@ abstract class SettingsStoreBase with Store { reaction((_) => lookupsENS, (bool looksUpENS) => _sharedPreferences.setBool(PreferencesKey.lookupsENS, looksUpENS)); + // secure storage keys: + reaction( + (_) => allowBiometricalAuthentication, + (bool biometricalAuthentication) => secureStorage.write( + key: SecureKey.allowBiometricalAuthenticationKey, + value: biometricalAuthentication.toString())); + + reaction( + (_) => selectedCake2FAPreset, + (Cake2FAPresetsOptions selectedCake2FAPreset) => secureStorage.write( + key: SecureKey.selectedCake2FAPreset, + value: selectedCake2FAPreset.serialize().toString())); + + reaction( + (_) => shouldRequireTOTP2FAForAccessingWallet, + (bool requireTOTP2FAForAccessingWallet) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAccessingWallet, + value: requireTOTP2FAForAccessingWallet.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToContact, + (bool requireTOTP2FAForSendsToContact) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToContact, + value: requireTOTP2FAForSendsToContact.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToNonContact, + (bool requireTOTP2FAForSendsToNonContact) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToNonContact, + value: requireTOTP2FAForSendsToNonContact.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToInternalWallets, + (bool requireTOTP2FAForSendsToInternalWallets) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets, + value: requireTOTP2FAForSendsToInternalWallets.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForExchangesToInternalWallets, + (bool requireTOTP2FAForExchangesToInternalWallets) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + value: requireTOTP2FAForExchangesToInternalWallets.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForExchangesToExternalWallets, + (bool requireTOTP2FAForExchangesToExternalWallets) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + value: requireTOTP2FAForExchangesToExternalWallets.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForAddingContacts, + (bool requireTOTP2FAForAddingContacts) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAddingContacts, + value: requireTOTP2FAForAddingContacts.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForCreatingNewWallets, + (bool requireTOTP2FAForCreatingNewWallets) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForCreatingNewWallets, + value: requireTOTP2FAForCreatingNewWallets.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + (bool requireTOTP2FAForAllSecurityAndBackupSettings) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + value: requireTOTP2FAForAllSecurityAndBackupSettings.toString())); + + reaction((_) => useTOTP2FA, + (bool use) => secureStorage.write(key: SecureKey.useTOTP2FA, value: use.toString())); + + reaction((_) => totpSecretKey, + (String totpKey) => secureStorage.write(key: SecureKey.totpSecretKey, value: totpKey)); + + reaction( + (_) => pinTimeOutDuration, + (PinCodeRequiredDuration pinCodeInterval) => secureStorage.write( + key: SecureKey.pinTimeOutDuration, value: pinCodeInterval.value.toString())); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -668,6 +679,7 @@ abstract class SettingsStoreBase with Store { String deviceName; + final FlutterSecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final BackgroundTasks _backgroundTasks; @@ -710,6 +722,7 @@ abstract class SettingsStoreBase with Store { BalanceDisplayMode initialBalanceDisplayMode = BalanceDisplayMode.availableBalance, ThemeBase? initialTheme}) async { final sharedPreferences = await getIt.getAsync(); + final secureStorage = await getIt.get(); final backgroundTasks = getIt.get(); final currentFiatCurrency = FiatCurrency.deserialize( raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!); @@ -770,36 +783,6 @@ abstract class SettingsStoreBase with Store { final currentFiatApiMode = FiatApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ?? FiatApiMode.enabled.raw); - final allowBiometricalAuthentication = - sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false; - final selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( - raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? - Cake2FAPresetsOptions.normal.raw); - final shouldRequireTOTP2FAForAccessingWallet = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false; - final shouldRequireTOTP2FAForSendsToContact = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false; - final shouldRequireTOTP2FAForSendsToNonContact = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false; - final shouldRequireTOTP2FAForSendsToInternalWallets = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ?? - false; - final shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? - false; - final shouldRequireTOTP2FAForExchangesToExternalWallets = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToExternalWallets) ?? - false; - final shouldRequireTOTP2FAForAddingContacts = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; - final shouldRequireTOTP2FAForCreatingNewWallets = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ?? - false; - final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ?? - false; - final useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? false; - final totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? ''; final tokenTrialNumber = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? 0; final shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? true; @@ -816,18 +799,14 @@ abstract class SettingsStoreBase with Store { actionListDisplayMode.addAll(deserializeActionlistDisplayModes( sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ?? defaultActionsMode)); var pinLength = sharedPreferences.getInt(PreferencesKey.currentPinLength); - final timeOutDuration = sharedPreferences.getInt(PreferencesKey.pinTimeOutDuration); - final seedPhraseCount = sharedPreferences.getInt(PreferencesKey.currentSeedPhraseLength); - final pinCodeTimeOutDuration = timeOutDuration != null - ? PinCodeRequiredDuration.deserialize(raw: timeOutDuration) - : defaultPinCodeTimeOutDuration; - final seedPhraseWordCount = seedPhraseCount != null - ? SeedPhraseLength.deserialize(raw: seedPhraseCount) - : defaultSeedPhraseLength; final sortBalanceBy = SortBalanceBy.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? 0]; final pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; + final seedPhraseCount = sharedPreferences.getInt(PreferencesKey.currentSeedPhraseLength); + final seedPhraseWordCount = seedPhraseCount != null + ? SeedPhraseLength.deserialize(raw: seedPhraseCount) + : defaultSeedPhraseLength; final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; final usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; @@ -929,7 +908,101 @@ abstract class SettingsStoreBase with Store { }); final savedSyncAll = sharedPreferences.getBool(PreferencesKey.syncAllKey) ?? true; + // migrated to secure: + final timeOutDuration = await SecureKey.getInt( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.pinTimeOutDuration, + ); + + final pinCodeTimeOutDuration = timeOutDuration != null + ? PinCodeRequiredDuration.deserialize(raw: timeOutDuration) + : defaultPinCodeTimeOutDuration; + + final allowBiometricalAuthentication = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.pinTimeOutDuration, + ) ?? + false; + + final selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: await SecureKey.getInt( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.selectedCake2FAPreset, + ) ?? + Cake2FAPresetsOptions.normal.raw); + + final shouldRequireTOTP2FAForAccessingWallet = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAccessingWallet, + ) ?? + false; + final shouldRequireTOTP2FAForSendsToContact = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToContact, + ) ?? + false; + final shouldRequireTOTP2FAForSendsToNonContact = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToNonContact, + ) ?? + false; + final shouldRequireTOTP2FAForSendsToInternalWallets = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets, + ) ?? + false; + final shouldRequireTOTP2FAForExchangesToInternalWallets = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + ) ?? + false; + final shouldRequireTOTP2FAForExchangesToExternalWallets = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + ) ?? + false; + final shouldRequireTOTP2FAForAddingContacts = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAddingContacts, + ) ?? + false; + final shouldRequireTOTP2FAForCreatingNewWallets = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForCreatingNewWallets, + ) ?? + false; + final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ) ?? + false; + final useTOTP2FA = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.useTOTP2FA, + ) ?? + false; + final totpSecretKey = await SecureKey.getString( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.totpSecretKey, + ) ?? + ''; + return SettingsStore( + secureStorage: secureStorage, sharedPreferences: sharedPreferences, initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard, nodes: nodes, @@ -1007,34 +1080,37 @@ abstract class SettingsStoreBase with Store { priority[WalletType.monero] = monero?.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!) ?? priority[WalletType.monero]!; - priority[WalletType.bitcoin] = bitcoin?.deserializeBitcoinTransactionPriority( - sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!) ?? - priority[WalletType.bitcoin]!; - if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { - priority[WalletType.haven] = monero?.deserializeMoneroTransactionPriority( - raw: sharedPreferences.getInt(PreferencesKey.havenTransactionPriority)!) ?? - priority[WalletType.haven]!; + if (bitcoin != null && + sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority) != null) { + priority[WalletType.bitcoin] = bitcoin!.deserializeBitcoinTransactionPriority( + sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority)!); } - if (sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority) != null) { - priority[WalletType.litecoin] = bitcoin?.deserializeLitecoinTransactionPriority( - sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!) ?? - priority[WalletType.litecoin]!; + + if (monero != null && + sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { + priority[WalletType.haven] = monero!.deserializeMoneroTransactionPriority( + raw: sharedPreferences.getInt(PreferencesKey.havenTransactionPriority)!); } - if (sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) { - priority[WalletType.ethereum] = ethereum?.deserializeEthereumTransactionPriority( - sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!) ?? - priority[WalletType.ethereum]!; + if (bitcoin != null && + sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority) != null) { + priority[WalletType.litecoin] = bitcoin!.deserializeLitecoinTransactionPriority( + sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!); } - if (sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority) != null) { - priority[WalletType.polygon] = polygon?.deserializePolygonTransactionPriority( - sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority)!) ?? - priority[WalletType.polygon]!; + if (ethereum != null && + sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) { + priority[WalletType.ethereum] = ethereum!.deserializeEthereumTransactionPriority( + sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!); } - if (sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { - priority[WalletType.bitcoinCash] = bitcoinCash?.deserializeBitcoinCashTransactionPriority( - sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!) ?? - priority[WalletType.bitcoinCash]!; + if (polygon != null && + sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority) != null) { + priority[WalletType.polygon] = polygon!.deserializePolygonTransactionPriority( + sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority)!); + } + if (bitcoinCash != null && + sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { + priority[WalletType.bitcoinCash] = bitcoinCash!.deserializeBitcoinCashTransactionPriority( + sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!); } final generateSubaddresses = @@ -1055,8 +1131,6 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? shouldSaveRecipientAddress; - useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? useTOTP2FA; - totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? totpSecretKey; numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; @@ -1065,41 +1139,10 @@ abstract class SettingsStoreBase with Store { walletListOrder = WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; - allowBiometricalAuthentication = - sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? - allowBiometricalAuthentication; - selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( - raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? - Cake2FAPresetsOptions.normal.raw); - shouldRequireTOTP2FAForAccessingWallet = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false; - shouldRequireTOTP2FAForSendsToContact = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false; - shouldRequireTOTP2FAForSendsToNonContact = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false; - shouldRequireTOTP2FAForSendsToInternalWallets = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ?? - false; - shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? - false; - shouldRequireTOTP2FAForExchangesToExternalWallets = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToExternalWallets) ?? - false; - shouldRequireTOTP2FAForAddingContacts = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; - shouldRequireTOTP2FAForCreatingNewWallets = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ?? - false; - shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ?? - false; + shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? shouldShowMarketPlaceInDashboard; - selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( - raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? - Cake2FAPresetsOptions.narrow.raw); exchangeStatus = ExchangeApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); @@ -1147,7 +1190,6 @@ abstract class SettingsStoreBase with Store { final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); - final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1188,6 +1230,93 @@ abstract class SettingsStoreBase with Store { if (nanoNode != null) { nodes[WalletType.nano] = nanoNode; } + + // MIGRATED: + + useTOTP2FA = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.useTOTP2FA, + ) ?? + useTOTP2FA; + + totpSecretKey = await SecureKey.getString( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.totpSecretKey, + ) ?? + totpSecretKey; + + allowBiometricalAuthentication = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.allowBiometricalAuthenticationKey, + ) ?? + allowBiometricalAuthentication; + + selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: await SecureKey.getInt( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.selectedCake2FAPreset, + ) ?? + Cake2FAPresetsOptions.normal.raw); + + shouldRequireTOTP2FAForAccessingWallet = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAccessingWallet, + ) ?? + false; + shouldRequireTOTP2FAForSendsToContact = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToContact, + ) ?? + false; + + shouldRequireTOTP2FAForSendsToNonContact = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToNonContact, + ) ?? + false; + shouldRequireTOTP2FAForSendsToInternalWallets = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets, + ) ?? + false; + shouldRequireTOTP2FAForExchangesToInternalWallets = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + ) ?? + false; + shouldRequireTOTP2FAForExchangesToExternalWallets = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + ) ?? + false; + shouldRequireTOTP2FAForAddingContacts = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAddingContacts, + ) ?? + false; + shouldRequireTOTP2FAForCreatingNewWallets = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForCreatingNewWallets, + ) ?? + false; + shouldRequireTOTP2FAForAllSecurityAndBackupSettings = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ) ?? + false; } Future _saveCurrentNode(Node node, WalletType walletType) async { diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 12c5d1f87..371b6fb9b 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -265,7 +265,7 @@ abstract class OutputBase with Store { Future fetchParsedAddress(BuildContext context) async { final domain = address; final ticker = cryptoCurrencyHandler().title.toLowerCase(); - parsedAddress = await getIt.get().resolve(domain, ticker); + parsedAddress = await getIt.get().resolve(context, domain, ticker); extractedAddress = await extractAddressFromParsed(context, parsedAddress); note = parsedAddress.description; } diff --git a/lib/view_model/set_up_2fa_viewmodel.dart b/lib/view_model/set_up_2fa_viewmodel.dart index 9587e3075..7347b32a8 100644 --- a/lib/view_model/set_up_2fa_viewmodel.dart +++ b/lib/view_model/set_up_2fa_viewmodel.dart @@ -27,7 +27,9 @@ abstract class Setup2FAViewModelBase with Store { unhighlightTabs = false, selected2FASettings = ObservableList(), state = InitialExecutionState() { - selectCakePreset(selectedCake2FAPreset); + if (selectedCake2FAPreset != Cake2FAPresetsOptions.none) { + selectCakePreset(selectedCake2FAPreset); + } reaction((_) => state, _saveLastAuthTime); } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index e4dd86b37..65375b3e7 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -38,7 +38,11 @@ abstract class PrivacySettingsViewModelBase with Store { } } - bool get isAutoGenerateSubaddressesVisible => _wallet.type == WalletType.monero; + bool get isAutoGenerateSubaddressesVisible => + _wallet.type == WalletType.monero || + _wallet.type == WalletType.bitcoin || + _wallet.type == WalletType.litecoin || + _wallet.type == WalletType.bitcoinCash; @computed bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress; diff --git a/lib/view_model/settings/security_settings_view_model.dart b/lib/view_model/settings/security_settings_view_model.dart index 5ea4dd4ea..b8812e224 100644 --- a/lib/view_model/settings/security_settings_view_model.dart +++ b/lib/view_model/settings/security_settings_view_model.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -9,14 +8,10 @@ part 'security_settings_view_model.g.dart'; class SecuritySettingsViewModel = SecuritySettingsViewModelBase with _$SecuritySettingsViewModel; abstract class SecuritySettingsViewModelBase with Store { - SecuritySettingsViewModelBase( - this._settingsStore, - this._authService, - ) : _biometricAuth = BiometricAuth(); + SecuritySettingsViewModelBase(this._settingsStore) : _biometricAuth = BiometricAuth(); final BiometricAuth _biometricAuth; final SettingsStore _settingsStore; - final AuthService _authService; @computed bool get allowBiometricalAuthentication => _settingsStore.allowBiometricalAuthentication; @@ -41,8 +36,6 @@ abstract class SecuritySettingsViewModelBase with Store { _settingsStore.allowBiometricalAuthentication = value; @action - setPinCodeRequiredDuration(PinCodeRequiredDuration duration) => + void setPinCodeRequiredDuration(PinCodeRequiredDuration duration) => _settingsStore.pinTimeOutDuration = duration; - - bool checkPinCodeRiquired() => _authService.requireAuth(); } diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 9e2aa7187..0b4f969cb 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -1,6 +1,5 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:mobx/mobx.dart'; -import 'package:flutter/foundation.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -33,7 +32,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { state = AddressEditOrCreateStateInitial(), label = item?.name ?? '', _item = item, - _wallet = wallet; + _wallet = wallet; @observable AddressEditOrCreateState state; @@ -46,6 +45,10 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { final WalletAddressListItem? _item; final WalletBase _wallet; + bool get isElectrum => _wallet.type == WalletType.bitcoin || + _wallet.type == WalletType.bitcoinCash || + _wallet.type == WalletType.litecoin; + Future save() async { try { state = AddressIsSaving(); @@ -65,12 +68,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { Future _createNew() async { final wallet = _wallet; - if (wallet.type == WalletType.bitcoin - || wallet.type == WalletType.litecoin - || wallet.type == WalletType.bitcoinCash) { - await bitcoin!.generateNewAddress(wallet); - await wallet.save(); - } + if (isElectrum) await bitcoin!.generateNewAddress(wallet, label); if (wallet.type == WalletType.monero) { await monero @@ -96,10 +94,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { Future _update() async { final wallet = _wallet; - /*if (wallet is BitcoinWallet) { - await wallet.walletAddresses.updateAddress(_item.address as String); - await wallet.save(); - }*/ + if (isElectrum) await bitcoin!.updateAddress(wallet, _item!.address, label); + final index = _item?.id; if (index != null) { if (wallet.type == WalletType.monero) { diff --git a/lib/view_model/wallet_address_list/wallet_address_list_item.dart b/lib/view_model/wallet_address_list/wallet_address_list_item.dart index ba7b93cf9..1152f1404 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_item.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_item.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/utils/list_item.dart'; class WalletAddressListItem extends ListItem { @@ -6,13 +5,19 @@ class WalletAddressListItem extends ListItem { required this.address, required this.isPrimary, this.id, - this.name}) + this.name, + this.txCount, + this.balance, + this.isChange = false}) : super(); final int? id; final bool isPrimary; final String address; final String? name; + final int? txCount; + final String? balance; + final bool isChange; @override String toString() => name ?? address; diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index ade279124..9270d1d44 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,21 +1,25 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; -import 'package:cw_core/currency.dart'; -import 'package:intl/intl.dart'; -import 'package:mobx/mobx.dart'; import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cw_core/amount_converter.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/haven/haven.dart'; +import 'package:intl/intl.dart'; +import 'package:mobx/mobx.dart'; part 'wallet_address_list_view_model.g.dart'; @@ -110,7 +114,8 @@ class EthereumURI extends PaymentURI { class BitcoinCashURI extends PaymentURI { BitcoinCashURI({required String amount, required String address}) - : super(amount: amount, address: address); + : super(amount: amount, address: address); + @override String toString() { var base = address; @@ -121,9 +126,7 @@ class BitcoinCashURI extends PaymentURI { return base; } - } - - +} class NanoURI extends PaymentURI { NanoURI({required String amount, required String address}) @@ -167,6 +170,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo hasAccounts = appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, amount = '', + _settingsStore = appStore.settingsStore, super(appStore: appStore) { _init(); } @@ -184,12 +188,28 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final NumberFormat _cryptoNumberFormat; final FiatConversionStore fiatConversionStore; + final SettingsStore _settingsStore; List get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; + String get buttonTitle { + if (isElectrumWallet) { + return S.current.addresses; + } + + if (isAutoGenerateSubaddressEnabled) { + return hasAccounts ? S.current.accounts : S.current.account; + } + + return hasAccounts ? S.current.accounts_subaddresses : S.current.addresses; + } + @observable Currency selectedCurrency; + @observable + String searchText = ''; + @computed int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency); @@ -277,14 +297,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.addAll(addressItems); } - if (wallet.type == WalletType.bitcoin) { - final primaryAddress = bitcoin!.getAddress(wallet); - final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) { - final isPrimary = addr == primaryAddress; + if (isElectrumWallet) { + final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { + final isPrimary = subaddress.id == 0; - return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr); + return WalletAddressListItem( + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.name, + address: subaddress.address, + txCount: subaddress.txCount, + balance: AmountConverter.amountIntToString( + walletTypeToCryptoCurrency(type), subaddress.balance), + isChange: subaddress.isChange); }); - addressList.addAll(bitcoinAddresses); + addressList.addAll(addressItems); } if (wallet.type == WalletType.ethereum) { @@ -299,6 +326,15 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (searchText.isNotEmpty) { + return ObservableList.of(addressList.where((item) { + if (item is WalletAddressListItem) { + return item.address.toLowerCase().contains(searchText.toLowerCase()); + } + return false; + })); + } + return addressList; } @@ -321,15 +357,23 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed bool get hasAddressList => wallet.type == WalletType.monero || - wallet.type == WalletType.haven;/* || - wallet.type == WalletType.nano || - wallet.type == WalletType.banano;*/// TODO: nano accounts are disabled for now + wallet.type == WalletType.haven || + wallet.type == WalletType.bitcoinCash || + wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin; + + // wallet.type == WalletType.nano || + // wallet.type == WalletType.banano; TODO: nano accounts are disabled for now @computed - bool get showElectrumAddressDisclaimer => + bool get isElectrumWallet => wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin || - wallet.type == WalletType.bitcoinCash; + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash; + + @computed + bool get isAutoGenerateSubaddressEnabled => + _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled; List _baseItems; @@ -343,9 +387,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _baseItems = []; if (wallet.type == WalletType.monero || - wallet.type == WalletType.haven /*|| + wallet.type == + WalletType + .haven /*|| wallet.type == WalletType.nano || - wallet.type == WalletType.banano*/) { + wallet.type == WalletType.banano*/ + ) { _baseItems.add(WalletAccountListHeader()); } @@ -367,6 +414,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } } + @action + void updateSearchText(String text) { + searchText = text; + } + void _convertAmountToCrypto() { final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); try { diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 407b6d3bc..b31133c7d 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:hive/hive.dart'; @@ -18,7 +17,6 @@ abstract class WalletListViewModelBase with Store { this._walletInfoSource, this._appStore, this._walletLoadingService, - this._authService, ) : wallets = ObservableList() { setOrderType(_appStore.settingsStore.walletListOrder); reaction((_) => _appStore.wallet, (_) => updateList()); @@ -39,7 +37,6 @@ abstract class WalletListViewModelBase with Store { final AppStore _appStore; final Box _walletInfoSource; final WalletLoadingService _walletLoadingService; - final AuthService _authService; WalletType get currentWalletType => _appStore.wallet!.type; @@ -160,8 +157,4 @@ abstract class WalletListViewModelBase with Store { break; } } - - bool checkIfAuthRequired() { - return _authService.requireAuth(); - } } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index c158c737e..fcbe1d733 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -106,7 +106,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus_macos: f6e86fd000e971d361e54b5afcadc8c8fa773308 - cw_monero: ec03de55a19c4a2b174ea687e0f4202edc716fa4 + cw_monero: f8b7f104508efba2591548e76b5c058d05cba3f0 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 45f675043..f38482ec3 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -105,6 +105,7 @@ dependencies: socks5_proxy: ^1.0.4 flutter_svg: ^2.0.9 polyseed: ^0.0.2 + nostr_tools: ^1.0.9 dev_dependencies: flutter_test: @@ -123,6 +124,11 @@ dev_dependencies: url: https://github.com/cake-tech/google-translator.git version: 1.0.0 +dependency_overrides: + bech32: + git: + url: https://github.com/cake-tech/bech32.git + flutter_icons: image_path: "assets/images/app_logo.png" android: true diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index b952f3cc9..dc7ec25e3 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -756,12 +756,18 @@ "dfx_option_description": "ﺎﺑﻭﺭﻭﺃ ﻲﻓ ﺕﺎﻛﺮﺸﻟﺍﻭ ﺔﺋﺰﺠﺘﻟﺍ ءﻼﻤﻌﻟ .ﻲﻓﺎﺿﺇ KYC ﻥﻭﺪﺑ ﻭﺭﻮﻳ 990 ﻰﻟﺇ ﻞﺼﻳ ﺎﻣ .ﻱﺮﺴﻳﻮﺴﻟﺍ", "polygonscan_history": "ﻥﺎﻜﺴﻧﻮﺠﻴﻟﻮﺑ ﺦﻳﺭﺎﺗ", "wallet_seed_legacy": "بذرة محفظة قديمة", - "default_sell_provider": " ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ", + "default_sell_provider": "ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ", "select_sell_provider_notice": ".ﻖﻴﺒﻄﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻚﺑ ﺹﺎﺨﻟﺍ ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ ﻦﻴﻴﻌﺗ ﻖﻳﺮﻃ ﻦﻋ ﺔﺷﺎﺸﻟﺍ ﻩﺬﻫ ﻲﻄﺨﺗ", "custom_drag": "مخصص (عقد وسحب)", "switchToEVMCompatibleWallet": " (Ethereum، Polygon) ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ EVM ﻊﻣ ﺔﻘﻓﺍﻮﺘﻣ ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ", "receivable_balance": "التوازن القادم", "confirmed_tx": "مؤكد", "transaction_details_source_address": "عنوان المصدر", - "track": " ﺭﺎﺴﻣ" + "track": " ﺭﺎﺴﻣ", + "pause_wallet_creation": ".ﺎﻴًﻟﺎﺣ ﺎﺘًﻗﺆﻣ ﺔﻔﻗﻮﺘﻣ Haven Wallet ءﺎﺸﻧﺇ ﻰﻠﻋ ﺓﺭﺪﻘﻟﺍ", + "camera_consent": ".ﻞﻴﺻﺎﻔﺘﻟﺍ ﻰﻠﻋ ﻝﻮﺼﺤﻠﻟ ﻢﻬﺑ ﺔﺻﺎﺨﻟﺍ ﺔﻴﺻﻮﺼﺨﻟﺍ ﺔﺳﺎﻴﺳ ﻦﻣ ﻖﻘﺤﺘﻟﺍ ﻰﺟﺮﻳ .${provider} ﻝﻮﻠ", + "no_relays": " ﺕﻼﺣﺮﻤﻟﺍ ﻻ", + "choose_relay": " ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ", + "no_relays_message": ".ﻪﺑ ﺹﺎﺨﻟﺍ Nostr ﻞﺠﺳ ﻰﻟﺇ ﺕﻼﺣﺮﻤﻟﺍ ﺔﻓﺎﺿﻹ ﻢﻠﺘﺴﻤﻟﺍ ﺩﺎﺷﺭﺇ ﻰﺟﺮﻳ .ﺕﻼﺣﺮﻣ ﻱﺃ ﻰﻠﻋ ﻱﻮﺘﺤﻳ ﻻ", + "no_relay_on_domain": ".ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﺡﺎﺘﻣ ﺮﻴﻏ ﻞﻴﺣﺮﺘﻟﺍ ﻥﺃ ﻭﺃ ﻡﺪﺨﺘﺴﻤﻟﺍ ﻝﺎﺠﻤﻟ ﻞﻴﺣﺮﺗ ﺪ" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index a0c0097f9..bdaeba803 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -759,5 +759,11 @@ "receivable_balance": "Баланс за вземания", "confirmed_tx": "Потвърдено", "transaction_details_source_address": "Адрес на източника", - "track": "Писта" + "track": "Писта", + "pause_wallet_creation": "Възможността за създаване на Haven Wallet в момента е на пауза.", + "camera_consent": "Вашият фотоапарат ще бъде използван за заснемане на изображение с цел идентификация от ${provider}. Моля, проверете тяхната политика за поверителност за подробности.", + "no_relays": "Без релета", + "choose_relay": "Моля, изберете реле, което да използвате", + "no_relays_message": "Намерихме запис Nostr NIP-05 за този потребител, но той не съдържа релета. Моля, инструктирайте получателя да добави релета към своя Nostr запис.", + "no_relay_on_domain": "Няма реле за домейна на потребителя или релето не е налично. Моля, изберете реле, което да използвате." } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 995c6e96b..9ea3f3432 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -759,5 +759,11 @@ "receivable_balance": "Zůstatek pohledávek", "confirmed_tx": "Potvrzeno", "transaction_details_source_address": "Zdrojová adresa", - "track": "Dráha" + "track": "Dráha", + "pause_wallet_creation": "Možnost vytvářet Haven Wallet je momentálně pozastavena.", + "camera_consent": "Váš fotoaparát použije k pořízení snímku pro účely identifikace ${provider}. Podrobnosti najdete v jejich Zásadách ochrany osobních údajů.", + "no_relays": "Žádná relé", + "choose_relay": "Vyberte relé, které chcete použít", + "no_relays_message": "Pro tohoto uživatele jsme našli záznam Nostr NIP-05, který však neobsahuje žádná relé. Požádejte příjemce, aby přidal přenosy do svého záznamu Nostr.", + "no_relay_on_domain": "Pro doménu uživatele neexistuje přenos nebo je přenos nedostupný. Vyberte relé, které chcete použít." } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index ffabc453f..ee897bf09 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -767,5 +767,11 @@ "receivable_balance": "Forderungsbilanz", "confirmed_tx": "Bestätigt", "transaction_details_source_address": "Quelladresse", - "track": "Schiene" + "track": "Schiene", + "pause_wallet_creation": "Die Möglichkeit, Haven Wallet zu erstellen, ist derzeit pausiert.", + "camera_consent": "Mit Ihrer Kamera wird bis zum ${provider} ein Bild zur Identifizierung aufgenommen. Weitere Informationen finden Sie in deren Datenschutzbestimmungen.", + "no_relays": "Keine Relais", + "choose_relay": "Bitte wählen Sie ein zu verwendendes Relais aus", + "no_relays_message": "Wir haben einen Nostr NIP-05-Eintrag für diesen Benutzer gefunden, der jedoch keine Relays enthält. Bitte weisen Sie den Empfänger an, Relays zu seinem Nostr-Datensatz hinzuzufügen.", + "no_relay_on_domain": "Es gibt kein Relay für die Domäne des Benutzers oder das Relay ist nicht verfügbar. Bitte wählen Sie ein zu verwendendes Relais aus." } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index d76da300f..bad0c67d4 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -768,5 +768,11 @@ "receivable_balance": "Receivable Balance", "confirmed_tx": "Confirmed", "transaction_details_source_address": "Source address", - "track": "Track" + "track": "Track", + "pause_wallet_creation": "Ability to create Haven Wallet is currently paused.", + "camera_consent": "Your camera will be used to capture an image for identification purposes by ${provider}. Please check their Privacy Policy for details.", + "no_relays": "No relays", + "choose_relay": "Please choose a relay to use", + "no_relays_message": "We found a Nostr NIP-05 record for this user, but it does not contain any relays. Please instruct the recipient to add relays to their Nostr record.", + "no_relay_on_domain": "There isn't a relay for user's domain or the relay is unavailable. Please choose a relay to use." } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index e97470ae6..8c8a34165 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -767,5 +767,11 @@ "receivable_balance": "Saldo de cuentas por cobrar", "confirmed_tx": "Confirmado", "transaction_details_source_address": "Dirección de la fuente", - "track": "Pista" + "track": "Pista", + "pause_wallet_creation": "La capacidad para crear Haven Wallet está actualmente pausada.", + "camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulte su Política de privacidad para obtener más detalles.", + "no_relays": "Sin relevos", + "choose_relay": "Por favor elija un relé para usar", + "no_relays_message": "Encontramos un registro Nostr NIP-05 para este usuario, pero no contiene ningún relé. Indique al destinatario que agregue retransmisiones a su registro Nostr.", + "no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elija un relé para usar." } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index e9bd6624d..f0c72993a 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -767,5 +767,11 @@ "receivable_balance": "Solde de créances", "confirmed_tx": "Confirmé", "transaction_details_source_address": "Adresse source", - "track": "Piste" + "track": "Piste", + "pause_wallet_creation": "La possibilité de créer Haven Wallet est actuellement suspendue.", + "camera_consent": "Votre appareil photo sera utilisé pour capturer une image à des fins d'identification par ${provider}. Veuillez consulter leur politique de confidentialité pour plus de détails.", + "no_relays": "Pas de relais", + "choose_relay": "Veuillez choisir un relais à utiliser", + "no_relays_message": "Nous avons trouvé un enregistrement Nostr NIP-05 pour cet utilisateur, mais il ne contient aucun relais. Veuillez demander au destinataire d'ajouter des relais à son enregistrement Nostr.", + "no_relay_on_domain": "Il n'existe pas de relais pour le domaine de l'utilisateur ou le relais n'est pas disponible. Veuillez choisir un relais à utiliser." } diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index f568b96c8..19a4f0c82 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -749,5 +749,11 @@ "receivable_balance": "Daidaituwa da daidaituwa", "confirmed_tx": "Tabbatar", "transaction_details_source_address": "Adireshin Incord", - "track": "Waƙa" + "track": "Waƙa", + "pause_wallet_creation": "A halin yanzu an dakatar da ikon ƙirƙirar Haven Wallet.", + "camera_consent": "Za a yi amfani da kyamarar ku don ɗaukar hoto don dalilai na tantancewa ta ${provider}. Da fatan za a duba Manufar Sirri don cikakkun bayanai.", + "no_relays": "Babu relays", + "choose_relay": "Da fatan za a zaɓi gudun ba da sanda don amfani", + "no_relays_message": "Mun sami rikodin Nostr NIP-05 don wannan mai amfani, amma ba ya ƙunshe da kowane relays. Da fatan za a umurci mai karɓa ya ƙara relays zuwa rikodin su na Nostr.", + "no_relay_on_domain": "Babu gudun ba da sanda ga yankin mai amfani ko kuma ba a samu ba. Da fatan za a zaɓi gudun ba da sanda don amfani." } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 3747ffaa7..c819d210f 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -767,5 +767,11 @@ "receivable_balance": "प्राप्य शेष", "confirmed_tx": "की पुष्टि", "transaction_details_source_address": "स्रोत पता", - "track": "रास्ता" + "track": "रास्ता", + "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", + "camera_consent": "आपके कैमरे का उपयोग ${provider} द्वारा पहचान उद्देश्यों के लिए एक छवि कैप्चर करने के लिए किया जाएगा। विवरण के लिए कृपया उनकी गोपनीयता नीति जांचें।", + "no_relays": "कोई रिले नहीं", + "choose_relay": "कृपया उपयोग करने के लिए एक रिले चुनें", + "no_relays_message": "हमें इस उपयोगकर्ता के लिए एक Nostr NIP-05 रिकॉर्ड मिला, लेकिन इसमें कोई रिले नहीं है। कृपया प्राप्तकर्ता को अपने नॉस्ट्र रिकॉर्ड में रिले जोड़ने का निर्देश दें।", + "no_relay_on_domain": "उपयोगकर्ता के डोमेन के लिए कोई रिले नहीं है या रिले अनुपलब्ध है। कृपया उपयोग करने के लिए एक रिले चुनें।" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 22ab524e5..ca10f7bdb 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -765,5 +765,11 @@ "receivable_balance": "Stanje potraživanja", "confirmed_tx": "Potvrđen", "transaction_details_source_address": "Adresa izvora", - "track": "Staza" + "track": "Staza", + "pause_wallet_creation": "Mogućnost stvaranja novčanika Haven trenutno je pauzirana.", + "camera_consent": "Vaš će fotoaparat koristiti za snimanje slike u svrhu identifikacije od strane ${provider}. Pojedinosti potražite u njihovoj politici privatnosti.", + "no_relays": "Nema releja", + "choose_relay": "Odaberite relej za korištenje", + "no_relays_message": "Pronašli smo zapis Nostr NIP-05 za ovog korisnika, ali on ne sadrži nikakve releje. Uputite primatelja da doda releje u svoj Nostr zapis.", + "no_relay_on_domain": "Ne postoji relej za korisničku domenu ili je relej nedostupan. Odaberite relej za korištenje." } diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index f611c47f6..0354273a1 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -755,5 +755,11 @@ "receivable_balance": "Saldo piutang", "confirmed_tx": "Dikonfirmasi", "transaction_details_source_address": "Alamat sumber", - "track": "Melacak" + "track": "Melacak", + "pause_wallet_creation": "Kemampuan untuk membuat Haven Wallet saat ini dijeda.", + "camera_consent": "Kamera Anda akan digunakan untuk mengambil gambar untuk tujuan identifikasi oleh ${provider}. Silakan periksa Kebijakan Privasi mereka untuk detailnya.", + "no_relays": "Tidak ada relay", + "choose_relay": "Silakan pilih relai yang akan digunakan", + "no_relays_message": "Kami menemukan catatan Nostr NIP-05 untuk pengguna ini, tetapi tidak berisi relay apa pun. Harap instruksikan penerima untuk menambahkan relay ke catatan Nostr mereka.", + "no_relay_on_domain": "Tidak ada relai untuk domain pengguna atau relai tidak tersedia. Silakan pilih relai yang akan digunakan." } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index ee77e3a5d..0adc4b534 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -767,5 +767,11 @@ "receivable_balance": "Bilanciamento creditizio", "confirmed_tx": "Confermato", "transaction_details_source_address": "Indirizzo di partenza", - "track": "Traccia" + "track": "Traccia", + "pause_wallet_creation": "La possibilità di creare Haven Wallet è attualmente sospesa.", + "camera_consent": "La tua fotocamera verrà utilizzata per acquisire un'immagine a scopo identificativo da ${provider}. Si prega di controllare la loro Informativa sulla privacy per i dettagli.", + "no_relays": "Nessun relè", + "choose_relay": "Scegli un relè da utilizzare", + "no_relays_message": "Abbiamo trovato un record Nostr NIP-05 per questo utente, ma non contiene alcun relè. Si prega di indicare al destinatario di aggiungere inoltri al proprio record Nostr.", + "no_relay_on_domain": "Non esiste un inoltro per il dominio dell'utente oppure l'inoltro non è disponibile. Scegli un relè da utilizzare." } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 70c9a3c75..d225bfe3f 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -767,5 +767,11 @@ "receivable_balance": "売掛金残高", "confirmed_tx": "確認済み", "transaction_details_source_address": "ソースアドレス", - "track": "追跡" + "track": "追跡", + "pause_wallet_creation": "Haven Wallet を作成する機能は現在一時停止されています。", + "camera_consent": "あなたのカメラは、${provider}_ までに識別目的で画像を撮影するために使用されます。詳細については、プライバシー ポリシーをご確認ください。", + "no_relays": "リレーなし", + "choose_relay": "使用するリレーを選択してください", + "no_relays_message": "このユーザーの Nostr NIP-05 レコードが見つかりましたが、リレーは含まれていません。受信者に Nostr レコードにリレーを追加するよう指示してください。", + "no_relay_on_domain": "ユーザーのドメインのリレーが存在しないか、リレーが使用できません。使用するリレーを選択してください。" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 2454674b4..028409ffd 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -765,5 +765,11 @@ "receivable_balance": "채권 잔액", "confirmed_tx": "확인", "transaction_details_source_address": "소스 주소", - "track": "길" + "track": "길", + "pause_wallet_creation": "Haven Wallet 생성 기능이 현재 일시 중지되었습니다.", + "camera_consent": "귀하의 카메라는 ${provider}의 식별 목적으로 이미지를 캡처하는 데 사용됩니다. 자세한 내용은 해당 개인정보 보호정책을 확인하세요.", + "no_relays": "릴레이 없음", + "choose_relay": "사용할 릴레이를 선택해주세요", + "no_relays_message": "이 사용자에 대한 Nostr NIP-05 레코드를 찾았지만 릴레이가 포함되어 있지 않습니다. 수신자에게 Nostr 기록에 릴레이를 추가하도록 지시하십시오.", + "no_relay_on_domain": "사용자 도메인에 릴레이가 없거나 릴레이를 사용할 수 없습니다. 사용할 릴레이를 선택해주세요." } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index aa5e65251..6bdd07e1d 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -765,5 +765,11 @@ "receivable_balance": "လက်ကျန်ငွေ", "confirmed_tx": "အတည်ပြုသည်", "transaction_details_source_address": "အရင်းအမြစ်လိပ်စာ", - "track": "တစ်ပုဒ်" + "track": "တစ်ပုဒ်", + "pause_wallet_creation": "Haven Wallet ဖန်တီးနိုင်မှုကို လောလောဆယ် ခေတ္တရပ်ထားသည်။", + "camera_consent": "မှတ်ပုံတင်ခြင်းရည်ရွယ်ချက်များအတွက် ${provider} တွင် သင့်ကင်မရာကို အသုံးပြုပါမည်။ အသေးစိတ်အတွက် ၎င်းတို့၏ ကိုယ်ရေးကိုယ်တာမူဝါဒကို စစ်ဆေးပါ။", + "no_relays": "Relay မရှိပါ။", + "choose_relay": "အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။", + "no_relays_message": "ဤအသုံးပြုသူအတွက် Nostr NIP-05 မှတ်တမ်းကို ကျွန်ုပ်တို့တွေ့ရှိသော်လည်း ၎င်းတွင် မည်သည့် relays မှ မပါဝင်ပါ။ ကျေးဇူးပြု၍ လက်ခံသူကို ၎င်းတို့၏ Nostr မှတ်တမ်းတွင် ထပ်လောင်းထည့်ရန် ညွှန်ကြားပါ။", + "no_relay_on_domain": "အသုံးပြုသူ၏ဒိုမိန်းအတွက် ထပ်ဆင့်လွှင့်ခြင်း မရှိပါ သို့မဟုတ် ထပ်ဆင့်လွှင့်ခြင်း မရနိုင်ပါ။ အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index ff3e86895..f42834253 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -767,5 +767,11 @@ "receivable_balance": "Het saldo", "confirmed_tx": "Bevestigd", "transaction_details_source_address": "Bron adres", - "track": "Spoor" + "track": "Spoor", + "pause_wallet_creation": "De mogelijkheid om Haven Wallet te maken is momenteel onderbroken.", + "camera_consent": "Uw camera wordt gebruikt om vóór ${provider} een beeld vast te leggen voor identificatiedoeleinden. Raadpleeg hun privacybeleid voor meer informatie.", + "no_relays": "Geen relais", + "choose_relay": "Kies een relais dat u wilt gebruiken", + "no_relays_message": "We hebben een Nostr NIP-05-record voor deze gebruiker gevonden, maar deze bevat geen relays. Instrueer de ontvanger om relays toe te voegen aan zijn Nostr-record.", + "no_relay_on_domain": "Er is geen relay voor het domein van de gebruiker of de relay is niet beschikbaar. Kies een relais dat u wilt gebruiken." } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 22b67438f..3c7384d85 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -767,5 +767,11 @@ "receivable_balance": "Saldo należności", "confirmed_tx": "Potwierdzony", "transaction_details_source_address": "Adres źródłowy", - "track": "Ścieżka" + "track": "Ścieżka", + "pause_wallet_creation": "Możliwość utworzenia Portfela Haven jest obecnie wstrzymana.", + "camera_consent": "Twój aparat zostanie użyty do przechwycenia obrazu w celach identyfikacyjnych przez ${provider}. Aby uzyskać szczegółowe informacje, sprawdź ich Politykę prywatności.", + "no_relays": "Żadnych przekaźników", + "choose_relay": "Wybierz przekaźnik, którego chcesz użyć", + "no_relays_message": "Znaleźliśmy rekord Nostr NIP-05 dla tego użytkownika, ale nie zawiera on żadnych przekaźników. Poinstruuj odbiorcę, aby dodał przekaźniki do swojego rekordu Nostr.", + "no_relay_on_domain": "Brak przekaźnika dla domeny użytkownika lub przekaźnik jest niedostępny. Wybierz przekaźnik, którego chcesz użyć." } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index ce6e96507..7f4ff2753 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -766,5 +766,11 @@ "receivable_balance": "Saldo a receber", "confirmed_tx": "Confirmado", "transaction_details_source_address": "Endereço de Origem", - "track": "Acompanhar" + "track": "Acompanhar", + "pause_wallet_creation": "A capacidade de criar a Haven Wallet está atualmente pausada.", + "camera_consent": "Sua câmera será usada para capturar uma imagem para fins de identificação por ${provider}. Por favor, verifique a Política de Privacidade para obter detalhes.", + "no_relays": "Sem relés", + "choose_relay": "Escolha um relé para usar", + "no_relays_message": "Encontramos um registro Nostr NIP-05 para este usuário, mas ele não contém nenhum relé. Instrua o destinatário a adicionar retransmissões ao seu registro Nostr.", + "no_relay_on_domain": "Não há uma retransmissão para o domínio do usuário ou a retransmissão está indisponível. Escolha um relé para usar." } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 31d0991c4..a111a998d 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -767,5 +767,11 @@ "receivable_balance": "Баланс дебиторской задолженности", "confirmed_tx": "Подтвержденный", "transaction_details_source_address": "Адрес источника", - "track": "Отслеживать" + "track": "Отслеживать", + "pause_wallet_creation": "Возможность создания Haven Wallet в настоящее время приостановлена.", + "camera_consent": "Ваша камера будет использоваться для захвата изображения в целях идентификации ${provider}. Пожалуйста, ознакомьтесь с их Политикой конфиденциальности для получения подробной информации.", + "no_relays": "Нет реле", + "choose_relay": "Пожалуйста, выберите реле для использования", + "no_relays_message": "Мы нашли запись Nostr NIP-05 для этого пользователя, но она не содержит никаких реле. Попросите получателя добавить реле в свою запись Nostr.", + "no_relay_on_domain": "Для домена пользователя реле не существует или реле недоступно. Пожалуйста, выберите реле для использования." } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 14f5c826c..f77a31ff4 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -765,5 +765,11 @@ "receivable_balance": "ยอดลูกหนี้", "confirmed_tx": "ซึ่งยืนยันแล้ว", "transaction_details_source_address": "ที่อยู่แหล่งกำเนิด", - "track": "ติดตาม" + "track": "ติดตาม", + "pause_wallet_creation": "ขณะนี้ความสามารถในการสร้าง Haven Wallet ถูกหยุดชั่วคราว", + "camera_consent": "กล้องของคุณจะถูกนำมาใช้เพื่อจับภาพเพื่อวัตถุประสงค์ในการระบุตัวตนภายใน ${provider} โปรดตรวจสอบนโยบายความเป็นส่วนตัวเพื่อดูรายละเอียด", + "no_relays": "ไม่มีรีเลย์", + "choose_relay": "กรุณาเลือกรีเลย์ที่จะใช้", + "no_relays_message": "เราพบบันทึก Nostr NIP-05 สำหรับผู้ใช้รายนี้ แต่ไม่มีรีเลย์ใดๆ โปรดแนะนำให้ผู้รับเพิ่มรีเลย์ลงในบันทึก Nostr ของตน", + "no_relay_on_domain": "ไม่มีการส่งต่อสำหรับโดเมนของผู้ใช้ หรือการส่งต่อไม่พร้อมใช้งาน กรุณาเลือกรีเลย์ที่จะใช้" } diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index decea0593..c2f12767e 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -761,5 +761,11 @@ "receivable_balance": "Natatanggap na balanse", "confirmed_tx": "Nakumpirma", "transaction_details_source_address": "SOURCE ADDRESS", - "track": "Subaybayan" + "track": "Subaybayan", + "pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.", + "camera_consent": "Gagamitin ang iyong camera upang kumuha ng larawan para sa mga layunin ng pagkakakilanlan sa pamamagitan ng ${provider}. Pakisuri ang kanilang Patakaran sa Privacy para sa mga detalye.", + "no_relays": "Walang mga relay", + "choose_relay": "Mangyaring pumili ng relay na gagamitin", + "no_relays_message": "Nakakita kami ng Nostr NIP-05 record para sa user na ito, ngunit hindi ito naglalaman ng anumang mga relay. Mangyaring atasan ang tatanggap na magdagdag ng mga relay sa kanilang Nostr record.", + "no_relay_on_domain": "Walang relay para sa domain ng user o hindi available ang relay. Mangyaring pumili ng relay na gagamitin." } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 126b269b8..6bebe480e 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -765,5 +765,11 @@ "receivable_balance": "Alacak bakiyesi", "confirmed_tx": "Onaylanmış", "transaction_details_source_address": "Kaynak adresi", - "track": "İzlemek" + "track": "İzlemek", + "pause_wallet_creation": "Haven Cüzdanı oluşturma yeteneği şu anda duraklatıldı.", + "camera_consent": "Kameranız ${provider} tarihine kadar tanımlama amacıyla bir görüntü yakalamak için kullanılacaktır. Ayrıntılar için lütfen Gizlilik Politikalarını kontrol edin.", + "no_relays": "Röle yok", + "choose_relay": "Lütfen kullanmak için bir röle seçin", + "no_relays_message": "Bu kullanıcı için bir Nostr NIP-05 kaydı bulduk ancak bu kayıt herhangi bir aktarma içermiyor. Lütfen alıcıya Nostr kayıtlarına aktarma eklemesi talimatını verin.", + "no_relay_on_domain": "Kullanıcının alanı için bir geçiş yok veya geçiş kullanılamıyor. Lütfen kullanmak için bir röle seçin." } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index defdba828..b63f820ea 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -767,5 +767,11 @@ "receivable_balance": "Баланс дебіторської заборгованості", "confirmed_tx": "Підтверджений", "transaction_details_source_address": "Адреса джерела", - "track": "Відслідковувати" + "track": "Відслідковувати", + "pause_wallet_creation": "Можливість створення гаманця Haven зараз призупинено.", + "camera_consent": "Ваша камера використовуватиметься для зйомки зображення з метою ідентифікації ${provider}. Будь ласка, ознайомтеся з їхньою політикою конфіденційності, щоб дізнатися більше.", + "no_relays": "Без реле", + "choose_relay": "Будь ласка, виберіть реле для використання", + "no_relays_message": "Ми знайшли запис Nostr NIP-05 для цього користувача, але він не містить жодних реле. Будь ласка, попросіть одержувача додати реле до свого запису Nostr.", + "no_relay_on_domain": "Немає ретранслятора для домену користувача або ретранслятор недоступний. Будь ласка, виберіть реле для використання." } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 4e800fbd4..98a649dcc 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -759,5 +759,11 @@ "receivable_balance": "قابل وصول توازن", "confirmed_tx": "تصدیق", "transaction_details_source_address": "ماخذ ایڈریس", - "track": " ﮏﯾﺮﭨ" + "track": " ﮏﯾﺮﭨ", + "pause_wallet_creation": "Haven Wallet ۔ﮯﮨ ﻑﻮﻗﻮﻣ ﻝﺎﺤﻟﺍ ﯽﻓ ﺖﯿﻠﮨﺍ ﯽﮐ ﮯﻧﺎﻨﺑ", + "camera_consent": "۔ﮟﯿﮭﮑﯾﺩ ﯽﺴﯿﻟﺎﭘ ﯽﺴﯾﻮﯿﺋﺍﺮﭘ ﯽﮐ ﻥﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ﮯﯿﻟ ﮯﮐ ﺕﻼ${provider}ﯿﺼﻔﺗ ۔ﺎﮔ ﮯﺋﺎﺟ ﺎﯿﮐ ﻝﺎﻤﻌﺘﺳﺍ ﮯﯿﻟ", + "no_relays": " ۔ﮟﯿﮩﻧ ﮯﻠﯾﺭ ﯽﺋﻮﮐ", + "choose_relay": " ۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ", + "no_relays_message": "۔ﮟﯾﺮﮐ ﻞﻣﺎﺷ ﮯﻠﯾﺭ ﮟﯿﻣ ﮈﺭﺎﮑﯾﺭ ﺮﭩﺳﻮﻧ ﮯﻨﭘﺍ ﮦﻭ ﮧﮐ ﮟﯾﺩ ﺖﯾﺍﺪﮨ ﻮﮐ ﮦﺪﻨﻨﮐ ﻝﻮﺻﻭ ﻡﺮﮐ ﮦﺍﺮﺑ ۔", + "no_relay_on_domain": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﮟﯿﮩﻧ ﺏﺎﯿﺘﺳﺩ ﮯﻠﯾﺭ ﺎﯾ ﮯﮨ ﮟ" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index cdb3b79a4..a97ee8d63 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -761,5 +761,11 @@ "receivable_balance": "Iwontunws.funfun ti o gba", "confirmed_tx": "Jẹrisi", "transaction_details_source_address": "Adirẹsi orisun", - "track": "Orin" + "track": "Orin", + "pause_wallet_creation": "Agbara lati ṣẹda Haven Wallet ti wa ni idaduro lọwọlọwọ.", + "camera_consent": "Kamẹra rẹ yoo ṣee lo lati ya aworan kan fun awọn idi idanimọ nipasẹ ${provider}. Jọwọ ṣayẹwo Ilana Aṣiri wọn fun awọn alaye.", + "no_relays": "Ko si relays", + "choose_relay": "Jọwọ yan yii lati lo", + "no_relays_message": "A ri igbasilẹ Nostr NIP-05 fun olumulo yii, ṣugbọn ko ni eyikeyi awọn iṣipopada ninu. Jọwọ sọ fun olugba lati ṣafikun awọn isunmọ si igbasilẹ Nostr wọn.", + "no_relay_on_domain": "Ko si iṣipopada fun agbegbe olumulo tabi yiyi ko si. Jọwọ yan yii lati lo." } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 0e0a6587f..c662f4a1a 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -766,5 +766,11 @@ "receivable_balance": "应收余额", "confirmed_tx": "确认的", "transaction_details_source_address": "源地址", - "track": "追踪" + "track": "追踪", + "pause_wallet_creation": "创建 Haven 钱包的功能当前已暂停。", + "camera_consent": "${provider} 将使用您的相机拍摄图像以供识别之用。请查看他们的隐私政策了解详情。", + "no_relays": "无继电器", + "choose_relay": "请选择要使用的继电器", + "no_relays_message": "我们找到了该用户的 Nostr NIP-05 记录,但它不包含任何中继。请指示收件人将中继添加到他们的 Nostr 记录中。", + "no_relay_on_domain": "用户域没有中继或中继不可用。请选择要使用的继电器。" } diff --git a/tool/configure.dart b/tool/configure.dart index 9d99bf49a..ce894d197 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -64,6 +64,7 @@ import 'package:cw_core/output_info.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart';"""; const bitcoinCWHeaders = """ import 'package:cw_bitcoin/electrum_wallet.dart'; @@ -76,9 +77,27 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; +import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ + + class ElectrumSubAddress { + ElectrumSubAddress({ + required this.id, + required this.name, + required this.address, + required this.txCount, + required this.balance, + required this.isChange}); + final int id; + final String name; + final String address; + final int txCount; + final int balance; + final bool isChange; +} + abstract class Bitcoin { TransactionPriority getMediumTransactionPriority(); @@ -92,13 +111,16 @@ abstract class Bitcoin { TransactionPriority deserializeBitcoinTransactionPriority(int raw); TransactionPriority deserializeLitecoinTransactionPriority(int raw); int getFeeRate(Object wallet, TransactionPriority priority); - Future generateNewAddress(Object wallet); + Future generateNewAddress(Object wallet, String label); + Future updateAddress(Object wallet,String address, String label); Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}); Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}); List getAddresses(Object wallet); String getAddress(Object wallet); + List getSubAddresses(Object wallet); + String formatterBitcoinAmountToString({required int amount}); double formatterBitcoinAmountToDouble({required int amount}); int formatterStringDoubleToBitcoinAmount(String amount);