diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 17510e0de..4f8f3eab5 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1772,7 +1772,8 @@ abstract class ElectrumWalletBase final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true); final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false); - + walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); + await walletAddresses.saveAddressesInBox(); await Future.wait(addressesByType.map((addressRecord) async { final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip()); diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 2de86d780..61d4fdbc3 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -163,6 +163,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override set address(String addr) { + if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) { + return; + } if (addressPageType == SilentPaymentsAddresType.p2sp) { final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); @@ -174,12 +177,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } return; } - - final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); + try { + final addressRecord = _addresses.firstWhere( + (addressRecord) => addressRecord.address == addr, + ); previousAddressRecord = addressRecord; receiveAddresses.remove(addressRecord); receiveAddresses.insert(0, addressRecord); + } catch (e) { + print("ElectrumWalletAddressBase: set address ($addr): $e"); + } } @override diff --git a/cw_core/lib/subaddress.dart b/cw_core/lib/subaddress.dart index 8571544a9..4dba7c836 100644 --- a/cw_core/lib/subaddress.dart +++ b/cw_core/lib/subaddress.dart @@ -1,12 +1,22 @@ class Subaddress { - Subaddress({required this.id, required this.address, required this.label}); + Subaddress({ + required this.id, + required this.address, + required this.label, + this.balance = null, + this.txCount = null, + }); Subaddress.fromMap(Map map) : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String), this.address = (map['address'] ?? '') as String, - this.label = (map['label'] ?? '') as String; + this.label = (map['label'] ?? '') as String, + this.balance = (map['balance'] ?? '') as String?, + this.txCount = (map['txCount'] ?? '') as int?; final int id; final String address; final String label; + final String? balance; + final int? txCount; } diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index e987b5d0e..ca488cfed 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,26 +1,58 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; abstract class WalletAddresses { WalletAddresses(this.walletInfo) : addressesMap = {}, allAddressesMap = {}, - addressInfos = {}; + addressInfos = {}, + usedAddresses = {}, + hiddenAddresses = walletInfo.hiddenAddresses?.toSet() ?? {}, + manualAddresses = walletInfo.manualAddresses?.toSet() ?? {}; final WalletInfo walletInfo; String get address; + String get latestAddress { + if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) { + if (addressesMap.keys.length == 0) return address; + return addressesMap[addressesMap.keys.last] ?? address; + } + return _localAddress ?? address; + } + String? get primaryAddress => null; - set address(String address); + String? _localAddress; + + set address(String address) => _localAddress = address; + + String get addressForExchange => address; Map addressesMap; Map allAddressesMap; + Map get usableAddressesMap { + final tmp = addressesMap.map((key, value) => MapEntry(key, value)); // copy address map + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key)); + return tmp; + } + + Map get usableAllAddressesMap { + final tmp = allAddressesMap.map((key, value) => MapEntry(key, value)); // copy address map + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key)); + return tmp; + } + Map> addressInfos; - Set usedAddresses = {}; + Set usedAddresses; + + Set hiddenAddresses; + + Set manualAddresses; Future init(); @@ -32,6 +64,8 @@ abstract class WalletAddresses { walletInfo.addresses = addressesMap; walletInfo.addressInfos = addressInfos; walletInfo.usedAddresses = usedAddresses.toList(); + walletInfo.hiddenAddresses = hiddenAddresses.toList(); + walletInfo.manualAddresses = manualAddresses.toList(); if (walletInfo.isInBox) { await walletInfo.save(); diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 0b0e8e4c5..bd035e30a 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -189,6 +189,15 @@ class WalletInfo extends HiveObject { @HiveField(22) String? parentAddress; + + @HiveField(23) + List? hiddenAddresses; + + @HiveField(24) + List? manualAddresses; + + + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart index eeeb763cf..06de44dff 100644 --- a/cw_haven/lib/haven_wallet_addresses.dart +++ b/cw_haven/lib/haven_wallet_addresses.dart @@ -1,6 +1,7 @@ import 'package:cw_core/wallet_addresses_with_account.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/account.dart'; +import 'package:cw_haven/api/wallet.dart'; import 'package:cw_haven/haven_account_list.dart'; import 'package:cw_haven/haven_subaddress_list.dart'; import 'package:cw_core/subaddress.dart'; @@ -36,7 +37,7 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -81,8 +82,9 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount monero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, - ); + late String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); final int addressIndex; final int accountIndex; - String get label => monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + final int received; + final int txCount; + String get label { + final localLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + if (localLabel.startsWith("#$addressIndex")) return localLabel; // don't duplicate the ID if it was user-providen + return "#$addressIndex ${localLabel}".trim(); + } } +class TinyTransactionDetails { + TinyTransactionDetails({ + required this.address, + required this.amount, + }); + final List address; + final int amount; +} + +int lastWptr = 0; +int lastTxCount = 0; +List ttDetails = []; + List getAllSubaddresses() { + txhistory = monero.Wallet_history(wptr!); + final txCount = monero.TransactionHistory_count(txhistory!); + if (lastTxCount != txCount && lastWptr != wptr!.address) { + final List newttDetails = []; + lastTxCount = txCount; + lastWptr = wptr!.address; + for (var i = 0; i < txCount; i++) { + final tx = monero.TransactionHistory_transaction(txhistory!, index: i); + if (monero.TransactionInfo_direction(tx) == monero.TransactionInfo_Direction.Out) continue; + final subaddrs = monero.TransactionInfo_subaddrIndex(tx).split(","); + final account = monero.TransactionInfo_subaddrAccount(tx); + newttDetails.add(TinyTransactionDetails( + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), + amount: monero.TransactionInfo_amount(tx), + )); + } + ttDetails.clear(); + ttDetails.addAll(newttDetails); + } final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final list = List.generate(size, (index) { + final ttDetailsLocal = ttDetails.where((element) { + final address = getAddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + if (element.address.contains(address)) return true; + return false; + }).toList(); + int received = 0; + for (var i = 0; i < ttDetailsLocal.length; i++) { + received += ttDetailsLocal[i].amount; + } return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, + received: received, + txCount: ttDetailsLocal.length, ); }).reversed.toList(); if (list.length == 0) { - list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0)); + list.add( + Subaddress( + addressIndex: subaddress!.accountIndex, + accountIndex: 0, + received: 0, + txCount: 0, + )); } return list; } +int numSubaddresses(int subaccountIndex) { + return monero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex); +} + void addSubaddressSync({required int accountIndex, required String label}) { monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label); refreshSubaddresses(accountIndex: accountIndex); diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index bd8b46356..a308b682e 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -5,32 +5,42 @@ import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/wallet.dart'; import 'package:ffi/ffi.dart'; import 'package:monero/monero.dart' as monero; import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen; +import 'package:mutex/mutex.dart'; String getTxKey(String txId) { return monero.Wallet_getTxKey(wptr!, txid: txId); } - +final txHistoryMutex = Mutex(); monero.TransactionHistory? txhistory; - -void refreshTransactions() { +bool isRefreshingTx = false; +Future refreshTransactions() async { + if (isRefreshingTx == true) return; + isRefreshingTx = true; txhistory ??= monero.Wallet_history(wptr!); - monero.TransactionHistory_refresh(txhistory!); + final ptr = txhistory!.address; + await txHistoryMutex.acquire(); + await Isolate.run(() { + monero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); + }); + txHistoryMutex.release(); + isRefreshingTx = false; } int countOfTransactions() => monero.TransactionHistory_count(txhistory!); -List getAllTransactions() { +Future> getAllTransactions() async { List dummyTxs = []; - + + await txHistoryMutex.acquire(); txhistory ??= monero.Wallet_history(wptr!); - monero.TransactionHistory_refresh(txhistory!); int size = countOfTransactions(); final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index))); - + txHistoryMutex.release(); final accts = monero.Wallet_numSubaddressAccounts(wptr!); for (var i = 0; i < accts; i++) { final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i); @@ -45,6 +55,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -251,19 +263,28 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = monero.Wallet_address( + late final String subaddressLabel = monero.Wallet_getSubaddressLabel( wptr!, - accountIndex: 0, - addressIndex: 0, + accountIndex: accountIndex, + addressIndex: addressIndex, ); + late final String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); + late final List addressList = List.generate(addressIndexList.length, (index) => + getAddress( + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 10; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; @@ -309,6 +330,8 @@ class Transaction { amount = monero.TransactionInfo_amount(txInfo), paymentId = monero.TransactionInfo_paymentId(txInfo), accountIndex = monero.TransactionInfo_subaddrAccount(txInfo), + addressIndex = int.tryParse(monero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, + addressIndexList = monero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), blockheight = monero.TransactionInfo_blockHeight(txInfo), confirmations = monero.TransactionInfo_confirmations(txInfo), fee = monero.TransactionInfo_fee(txInfo), @@ -331,6 +354,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndexList, + required this.addressIndex, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index b493e536e..8e03cff3e 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -66,9 +66,20 @@ String getSeedLegacy(String? language) { return legacy; } -String getAddress({int accountIndex = 0, int addressIndex = 0}) => - monero.Wallet_address(wptr!, +Map>> addressCache = {}; + +String getAddress({int accountIndex = 0, int addressIndex = 0}) { + // print("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}"); + while (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + print("adding subaddress"); + monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + } + addressCache[wptr!.address] ??= {}; + addressCache[wptr!.address]![accountIndex] ??= {}; + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + return addressCache[wptr!.address]![accountIndex]![addressIndex]!; +} int getFullBalance({int accountIndex = 0}) => monero.Wallet_balance(wptr!, accountIndex: accountIndex); diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index c35afb282..c20b23b5e 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_monero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -54,18 +55,12 @@ abstract class MoneroSubaddressListBase with Store { final address = s.address; final label = s.label; final id = s.addressIndex; - final hasDefaultAddressName = - label.toLowerCase() == 'Primary account'.toLowerCase() || - label.toLowerCase() == 'Untitled account'.toLowerCase(); - final isPrimaryAddress = id == 0 && hasDefaultAddressName; return Subaddress( id: id, address: address, - label: isPrimaryAddress - ? 'Primary address' - : hasDefaultAddressName - ? '' - : label); + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, + label: label); }).toList(); } @@ -103,6 +98,9 @@ abstract class MoneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -124,7 +122,8 @@ abstract class MoneroSubaddressListBase with Store { Future> _getAllUnusedAddresses( {required int accountIndex, required String label}) async { final allAddresses = subaddress_list.getAllSubaddresses(); - if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last)) { + // first because addresses come in reversed order. + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.first.address)) { final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); if (!isAddressUnused) { return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); @@ -139,12 +138,13 @@ abstract class MoneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' : label); - }) - .toList(); + }).toList().reversed.toList(); } Future _newSubaddress({required int accountIndex, required String label}) async { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 8773d694d..c6d5d2e5f 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -59,7 +59,7 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -128,6 +131,7 @@ abstract class MoneroWalletBase extends WalletBase await save()); + // update transaction details after restore + walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0); } @override @@ -167,6 +173,7 @@ abstract class MoneroWalletBase extends WalletBase> fetchTransactions() async { transaction_history.refreshTransactions(); - return _getAllTransactionsOfAccount(walletAddresses.account?.id) + return (await _getAllTransactionsOfAccount(walletAddresses.account?.id)) .fold>( {}, (Map acc, MoneroTransactionInfo tx) { @@ -594,8 +601,8 @@ abstract class MoneroWalletBase extends WalletBase monero_wallet.getSubaddressLabel(accountIndex, addressIndex); - List _getAllTransactionsOfAccount(int? accountIndex) => - transaction_history - .getAllTransactions() + Future> _getAllTransactionsOfAccount(int? accountIndex) async => + (await transaction_history + .getAllTransactions()) .map( (row) => MoneroTransactionInfo( row.hash, diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index d4f22e46f..c8a4448a4 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_account_list.dart'; import 'package:cw_monero/monero_subaddress_list.dart'; @@ -27,6 +29,30 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get latestAddress { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + subaddressList.update(accountIndex: account?.id??0); + } + return address; + } + + @override + String get addressForExchange { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address) || manualAddresses.contains(address) || subaddress_list.getRawLabel(accountIndex: account?.id??0, addressIndex: addressIndex).isNotEmpty) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + subaddressList.update(accountIndex: account?.id??0); + } + return address; + } + @observable Account? account; @@ -37,10 +63,12 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { MoneroAccountList accountList; + Set usedAddresses = Set(); + @override Future init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -89,8 +117,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { void updateSubaddressList({required int accountIndex}) { subaddressList.update(accountIndex: accountIndex); - subaddress = subaddressList.subaddresses.first; - address = subaddress!.address; + address = subaddressList.subaddresses.isNotEmpty + ? subaddressList.subaddresses.first.address + : getAddress(); } Future updateUsedSubaddress() async { @@ -109,7 +138,10 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last; + if (num.tryParse(subaddress!.balance??'0') != 0) { + getAddress(accountIndex: accountIndex, addressIndex: (subaddress?.id??0)+1); + } address = subaddress!.address; } diff --git a/cw_wownero/lib/api/subaddress_list.dart b/cw_wownero/lib/api/subaddress_list.dart index d8c91a584..b07f24c4a 100644 --- a/cw_wownero/lib/api/subaddress_list.dart +++ b/cw_wownero/lib/api/subaddress_list.dart @@ -1,4 +1,5 @@ import 'package:cw_wownero/api/account_list.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/wallet.dart'; import 'package:monero/wownero.dart' as wownero; @@ -28,27 +29,75 @@ class Subaddress { Subaddress({ required this.addressIndex, required this.accountIndex, + required this.txCount, + required this.received, }); - String get address => wownero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, - ); + late String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); final int addressIndex; final int accountIndex; String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + final int txCount; + final int received; } +class TinyTransactionDetails { + TinyTransactionDetails({ + required this.address, + required this.amount, + }); + final List address; + final int amount; +} + +int lastWptr = 0; +int lastTxCount = 0; +List ttDetails = []; + List getAllSubaddresses() { + txhistory = wownero.Wallet_history(wptr!); + final txCount = wownero.TransactionHistory_count(txhistory!); + if (lastTxCount != txCount && lastWptr != wptr!.address) { + final List newttDetails = []; + lastTxCount = txCount; + lastWptr = wptr!.address; + for (var i = 0; i < txCount; i++) { + final tx = wownero.TransactionHistory_transaction(txhistory!, index: i); + final subaddrs = wownero.TransactionInfo_subaddrIndex(tx).split(","); + final account = wownero.TransactionInfo_subaddrAccount(tx); + newttDetails.add(TinyTransactionDetails( + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), + amount: wownero.TransactionInfo_amount(tx), + )); + } + ttDetails.clear(); + ttDetails.addAll(newttDetails); + } final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final list = List.generate(size, (index) { + final ttDetailsLocal = ttDetails.where((element) { + final address = getAddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + if (address == element.address) return true; + return false; + }).toList(); + int received = 0; + for (var i = 0; i < ttDetailsLocal.length; i++) { + received += ttDetailsLocal[i].amount; + } return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, + received: received, + txCount: ttDetailsLocal.length, ); }).reversed.toList(); if (list.isEmpty) { - list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex)); + list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0)); } return list; } @@ -58,6 +107,10 @@ void addSubaddressSync({required int accountIndex, required String label}) { refreshSubaddresses(accountIndex: accountIndex); } +int numSubaddresses(int subaccountIndex) { + return wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex); +} + void setLabelForSubaddressSync( {required int accountIndex, required int addressIndex, required String label}) { wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label); diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart index a1e1e3c9b..6b0923e83 100644 --- a/cw_wownero/lib/api/transaction_history.dart +++ b/cw_wownero/lib/api/transaction_history.dart @@ -3,6 +3,7 @@ import 'dart:isolate'; import 'package:cw_wownero/api/account_list.dart'; import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart'; +import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/api/wownero_output.dart'; import 'package:cw_wownero/api/structs/pending_transaction.dart'; import 'package:ffi/ffi.dart'; @@ -16,9 +17,16 @@ String getTxKey(String txId) { wownero.TransactionHistory? txhistory; -void refreshTransactions() { +bool isRefreshingTx = false; +Future refreshTransactions() async { + if (isRefreshingTx == true) return; + isRefreshingTx = true; txhistory ??= wownero.Wallet_history(wptr!); - wownero.TransactionHistory_refresh(txhistory!); + final ptr = txhistory!.address; + await Isolate.run(() { + wownero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); + }); + isRefreshingTx = false; } int countOfTransactions() => wownero.TransactionHistory_count(txhistory!); @@ -45,6 +53,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -243,23 +253,28 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = wownero.Wallet_address( - wptr!, - accountIndex: 0, - addressIndex: 0, + late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + late final String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, ); + late final List addressList = List.generate(addressIndexList.length, (index) => + getAddress( + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 3; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; - late DateTime timeStamp; + late final DateTime timeStamp; late final bool isConfirmed = !isPending; final String hash; final String key; @@ -301,6 +316,8 @@ class Transaction { amount = wownero.TransactionInfo_amount(txInfo), paymentId = wownero.TransactionInfo_paymentId(txInfo), accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo), + addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, + addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), blockheight = wownero.TransactionInfo_blockHeight(txInfo), confirmations = wownero.TransactionInfo_confirmations(txInfo), fee = wownero.TransactionInfo_fee(txInfo), @@ -314,6 +331,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndex, + required this.addressIndexList, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 56f54dfac..baf9c8960 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -67,10 +67,19 @@ String getSeedLegacy(String? language) { } return legacy; } +Map>> addressCache = {}; -String getAddress({int accountIndex = 0, int addressIndex = 1}) => - wownero.Wallet_address(wptr!, +String getAddress({int accountIndex = 0, int addressIndex = 1}) { + while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + print("adding subaddress"); + wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + } + addressCache[wptr!.address] ??= {}; + addressCache[wptr!.address]![accountIndex] ??= {}; + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= wownero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + return addressCache[wptr!.address]![accountIndex]![addressIndex]!; +} int getFullBalance({int accountIndex = 0}) => wownero.Wallet_balance(wptr!, accountIndex: accountIndex); diff --git a/cw_wownero/lib/wownero_subaddress_list.dart b/cw_wownero/lib/wownero_subaddress_list.dart index 61fd09ef9..2ef4f2045 100644 --- a/cw_wownero/lib/wownero_subaddress_list.dart +++ b/cw_wownero/lib/wownero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_wownero/api/coins_info.dart'; import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_wownero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -61,6 +62,8 @@ abstract class WowneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: isPrimaryAddress ? 'Primary address' : hasDefaultAddressName @@ -103,6 +106,9 @@ abstract class WowneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -140,6 +146,8 @@ abstract class WowneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index c3f4bcb69..02d968eb3 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -59,7 +59,7 @@ abstract class WowneroWalletBase _isTransactionUpdating = false, _hasSyncAfterStartup = false, _password = password, - isEnabledAutoGenerateSubaddress = false, + isEnabledAutoGenerateSubaddress = true, syncStatus = NotConnectedSyncStatus(), unspentCoins = [], this.unspentCoinsInfo = unspentCoinsInfo, @@ -82,6 +82,10 @@ abstract class WowneroWalletBase reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -123,6 +127,7 @@ abstract class WowneroWalletBase wownero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; + ReactionDisposer? _onTxHistoryChangeReaction; bool _isTransactionUpdating; bool _hasSyncAfterStartup; Timer? _autoSaveTimer; @@ -158,6 +163,7 @@ abstract class WowneroWalletBase void close() async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); + _onTxHistoryChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); } @@ -564,8 +570,8 @@ abstract class WowneroWalletBase } _isTransactionUpdating = true; - transactionHistory.clear(); final transactions = await fetchTransactions(); + transactionHistory.clear(); transactionHistory.addMany(transactions); await transactionHistory.save(); _isTransactionUpdating = false; diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index 9eeb182eb..b2f9ec67a 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; +import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/wownero_account_list.dart'; import 'package:cw_wownero/wownero_subaddress_list.dart'; @@ -27,6 +29,27 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get latestAddress { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + } + return address; + } + + @override + String get addressForExchange { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address) || manualAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + } + return address; + } @observable Account? account; @@ -36,11 +59,14 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; + + @override + Set usedAddresses = Set(); @override Future init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -89,8 +115,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { void updateSubaddressList({required int accountIndex}) { subaddressList.update(accountIndex: accountIndex); - subaddress = subaddressList.subaddresses.first; - address = subaddress!.address; + address = subaddressList.subaddresses.isNotEmpty + ? subaddressList.subaddresses.first.address + : getAddress(); } Future updateUsedSubaddress() async { @@ -109,7 +136,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last; address = subaddress!.address; } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 1f1888b44..b1cf49482 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -61,7 +61,13 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { ObservableList get subaddresses { final moneroWallet = _wallet as MoneroWallet; final subAddresses = moneroWallet.walletAddresses.subaddressList.subaddresses - .map((sub) => Subaddress(id: sub.id, address: sub.address, label: sub.label)) + .map((sub) => Subaddress( + id: sub.id, + address: sub.address, + label: sub.label, + received: sub.balance??"unknown", + txCount: sub.txCount??0, + )) .toList(); return ObservableList.of(subAddresses); } @@ -83,7 +89,12 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { final moneroWallet = wallet as MoneroWallet; return moneroWallet.walletAddresses.subaddressList .getAll() - .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address)) + .map((sub) => Subaddress( + id: sub.id, + label: sub.label, + address: sub.address, + txCount: sub.txCount??0, + received: sub.balance??'unknown')) .toList(); } @@ -91,7 +102,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { Future addSubaddress(Object wallet, {required int accountIndex, required String label}) async { final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.subaddressList + return await moneroWallet.walletAddresses.subaddressList .addSubaddress(accountIndex: accountIndex, label: label); } diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 2040cbf9d..10f9aef43 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -155,13 +155,14 @@ class AddressPage extends BasePage { amountTextFieldFocusNode: _cryptoAmountFocus, amountController: _amountController, isLight: dashboardViewModel.settingsStore.currentTheme.type == - ThemeType.light))), + ThemeType.light, + ))), SizedBox(height: 16), Observer(builder: (_) { if (addressListViewModel.hasAddressList) { return SelectButton( text: addressListViewModel.buttonTitle, - onTap: () async => Navigator.of(context).pushNamed(Routes.receive), + onTap: () => Navigator.of(context).pushNamed(Routes.receive), textColor: Theme.of(context).extension()!.textColor, color: Theme.of(context).extension()!.syncedBackgroundColor, borderColor: Theme.of(context).extension()!.cardBorderColor, diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 78b4d0db8..2f8e3eb5c 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -509,7 +509,7 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) { + reaction((_) => exchangeViewModel.wallet.walletAddresses.addressForExchange, (String address) { if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) { depositKey.currentState!.changeAddress(address: address); } @@ -565,7 +565,7 @@ class ExchangePage extends BasePage { key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : ''); key.currentState!.changeAddress( - address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : ''); + address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : ''); key.currentState!.changeAmount(amount: ''); } @@ -576,9 +576,9 @@ class ExchangePage extends BasePage { if (isCurrentTypeWallet) { key.currentState!.changeWalletName(exchangeViewModel.wallet.name); - key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address; + key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.addressForExchange; } else if (key.currentState!.addressController.text == - exchangeViewModel.wallet.walletAddresses.address) { + exchangeViewModel.wallet.walletAddresses.addressForExchange) { key.currentState!.changeWalletName(''); key.currentState!.addressController.text = ''; } @@ -629,7 +629,7 @@ class ExchangePage extends BasePage { initialCurrency: exchangeViewModel.depositCurrency, initialWalletName: depositWalletName ?? '', initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.depositAddress, initialIsAmountEditable: true, initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, @@ -694,7 +694,7 @@ class ExchangePage extends BasePage { initialCurrency: exchangeViewModel.receiveCurrency, initialWalletName: receiveWalletName ?? '', initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.receiveAddress, initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable, isAmountEstimated: true, diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index 4edc9095a..f82318ca2 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -129,7 +129,7 @@ class ExchangeTemplatePage extends BasePage { initialWalletName: depositWalletName ?? '', initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.depositAddress, initialIsAmountEditable: true, initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, @@ -166,7 +166,7 @@ class ExchangeTemplatePage extends BasePage { initialWalletName: receiveWalletName ?? '', initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.receiveAddress, initialIsAmountEditable: false, isAmountEstimated: true, diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 724e5c3bd..7e3c2b555 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -121,7 +121,8 @@ class ReceivePage extends BasePage { heroTag: _heroTag, amountTextFieldFocusNode: _cryptoAmountFocus, amountController: _amountController, - isLight: currentTheme.type == ThemeType.light), + isLight: currentTheme.type == ThemeType.light, + ), ), AddressList(addressListViewModel: addressListViewModel), Padding( diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 850c08209..beef7c762 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -2,6 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -15,11 +16,14 @@ class AddressCell extends StatelessWidget { required this.textColor, this.onTap, this.onEdit, + this.onHide, + this.isHidden = false, this.onDelete, this.txCount, this.balance, this.isChange = false, - this.hasBalance = false}); + this.hasBalance = false, + this.hasReceived = false}); factory AddressCell.fromItem( WalletAddressListItem item, { @@ -28,7 +32,10 @@ class AddressCell extends StatelessWidget { required Color textColor, Function(String)? onTap, bool hasBalance = false, + bool hasReceived = false, Function()? onEdit, + Function()? onHide, + bool isHidden = false, Function()? onDelete, }) => AddressCell( @@ -40,11 +47,14 @@ class AddressCell extends StatelessWidget { textColor: textColor, onTap: onTap, onEdit: onEdit, + onHide: onHide, + isHidden: isHidden, onDelete: onDelete, txCount: item.txCount, balance: item.balance, isChange: item.isChange, - hasBalance: hasBalance); + hasBalance: hasBalance, + hasReceived: hasReceived,); final String address; final String name; @@ -54,11 +64,14 @@ class AddressCell extends StatelessWidget { final Color textColor; final Function(String)? onTap; final Function()? onEdit; + final Function()? onHide; + final bool isHidden; final Function()? onDelete; final int? txCount; final String? balance; final bool isChange; final bool hasBalance; + final bool hasReceived; static const int addressPreviewLength = 8; @@ -138,7 +151,7 @@ class AddressCell extends StatelessWidget { ), ], ), - if (hasBalance) + if (hasBalance || hasReceived) Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( @@ -146,7 +159,7 @@ class AddressCell extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Text( - '${S.of(context).balance}: $balance', + '${hasReceived ? S.of(context).received : S.of(context).balance}: $balance', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -178,14 +191,28 @@ class AddressCell extends StatelessWidget { enabled: !isCurrent, child: Slidable( key: Key(address), - startActionPane: _actionPane(context), - endActionPane: _actionPane(context), + startActionPane: _actionPaneStart(context), + endActionPane: _actionPaneEnd(context), child: cell, ), ); } - ActionPane _actionPane(BuildContext context) => ActionPane( + ActionPane _actionPaneEnd(BuildContext context) => ActionPane( + motion: const ScrollMotion(), + extentRatio: onDelete != null ? 0.4 : 0.3, + children: [ + SlidableAction( + onPressed: (_) => onHide?.call(), + backgroundColor: isHidden ? Colors.green : Colors.red, + foregroundColor: Colors.white, + icon: isHidden ? CupertinoIcons.arrow_left : CupertinoIcons.arrow_right, + label: isHidden ? S.of(context).show : S.of(context).hide, + ), + ], + ); + + ActionPane _actionPaneStart(BuildContext context) => ActionPane( motion: const ScrollMotion(), extentRatio: onDelete != null ? 0.4 : 0.3, children: [ diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 27ec8c33a..9f15018d0 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -10,16 +10,19 @@ import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart'; import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/utils/show_pop_up.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_hidden_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:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -class AddressList extends StatelessWidget { +class AddressList extends StatefulWidget { const AddressList({ super.key, required this.addressListViewModel, @@ -29,59 +32,123 @@ class AddressList extends StatelessWidget { final WalletAddressListViewModel addressListViewModel; final Function(String)? onSelect; + @override + State createState() => _AddressListState(); +} + +class _AddressListState extends State { + + bool showHiddenAddresses = false; + + void _toggleHiddenAddresses() { + setState(() { + showHiddenAddresses = !showHiddenAddresses; + }); + updateItems(); + } + + List getItems(List list, bool showHidden) { + return list.where((element) { + if (element is WalletAddressListItem) { + if (showHidden && element.isHidden) return true; + if (!showHidden && !element.isHidden) return true; + return false; + } + return true; + }).toList(); + } + + List items = []; + + void updateItems() { + setState(() { + items = getItems(widget.addressListViewModel.items, showHiddenAddresses); + }); + } + + @override + void initState() { + super.initState(); + + items = getItems(widget.addressListViewModel.items, showHiddenAddresses); + } + @override Widget build(BuildContext context) { - bool editable = onSelect == null; - return Observer( - builder: (_) => ListView.separated( - padding: EdgeInsets.all(0), - separatorBuilder: (context, _) => const HorizontalSectionDivider(), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: addressListViewModel.items.length, - itemBuilder: (context, index) { - final item = addressListViewModel.items[index]; - Widget cell = Container(); + bool editable = widget.onSelect == null; + return ListView.separated( + padding: EdgeInsets.all(0), + separatorBuilder: (context, _) => const HorizontalSectionDivider(), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + Widget cell = Container(); - if (item is WalletAccountListHeader) { - cell = HeaderTile( - showTrailingButton: true, - walletAddressListViewModel: addressListViewModel, - trailingButtonTap: () async { - if (addressListViewModel.type == WalletType.monero || - addressListViewModel.type == WalletType.haven) { - await showPopUp( - context: context, builder: (_) => getIt.get()); - } else { - await showPopUp( - context: context, builder: (_) => getIt.get()); - } - }, - title: S.of(context).accounts, - trailingIcon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).extension()!.iconsColor, - )); - } + if (item is WalletAccountListHeader) { + cell = HeaderTile( + showTrailingButton: true, + walletAddressListViewModel: widget.addressListViewModel, + trailingButtonTap: () async { + if (widget.addressListViewModel.type == WalletType.monero || + widget.addressListViewModel.type == WalletType.haven) { + await showPopUp( + context: context, builder: (_) => getIt.get()); + updateItems(); + } else { + await showPopUp( + context: context, builder: (_) => getIt.get()); + updateItems(); + } + }, + title: S.of(context).accounts, + trailingIcon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + )); + } - if (item is WalletAddressListHeader) { - 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 WalletAddressHiddenListHeader) { + cell = HeaderTile( + title: S.of(context).hidden_addresses, + walletAddressListViewModel: widget.addressListViewModel, + showTrailingButton: true, + showSearchButton: false, + trailingButtonTap: _toggleHiddenAddresses, + trailingIcon: Icon( + showHiddenAddresses ? Icons.toggle_on : Icons.toggle_off, + size: 20, + color: Theme.of(context).extension()!.iconsColor, + )); + } - if (item is WalletAddressListItem) { + if (item is WalletAddressListHeader) { + cell = HeaderTile( + title: S.of(context).addresses, + walletAddressListViewModel: widget.addressListViewModel, + showTrailingButton: widget.addressListViewModel.showAddManualAddresses, + showSearchButton: true, + onSearchCallback: updateItems, + trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { + updateItems(); // refresh the new address + }), + trailingIcon: Icon( + Icons.add, + size: 20, + color: Theme.of(context).extension()!.iconsColor, + )); + } + + if (item is WalletAddressListItem) { + if (item.isHidden && !showHiddenAddresses) { + cell = Container(); + } else if (!item.isHidden && showHiddenAddresses) { + cell = Container(); + } else { cell = Observer(builder: (_) { - final isCurrent = item.address == addressListViewModel.address.address && editable; + final isCurrent = item.address == widget.addressListViewModel.address.address && editable; final backgroundColor = isCurrent ? Theme.of(context).extension()!.currentTileBackgroundColor : Theme.of(context).extension()!.tilesBackgroundColor; @@ -89,35 +156,51 @@ class AddressList extends StatelessWidget { ? Theme.of(context).extension()!.currentTileTextColor : Theme.of(context).extension()!.tilesTextColor; + return AddressCell.fromItem( item, isCurrent: isCurrent, - hasBalance: addressListViewModel.isElectrumWallet, - backgroundColor: backgroundColor, + hasBalance: widget.addressListViewModel.isBalanceAvailable, + hasReceived: widget.addressListViewModel.isReceivedAvailable, + // hasReceived: + backgroundColor: (kDebugMode && item.isHidden) ? + Theme.of(context).colorScheme.error : + (kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) : + backgroundColor, textColor: textColor, onTap: (_) { - if (onSelect != null) { - onSelect!(item.address); + if (widget.onSelect != null) { + widget.onSelect!(item.address); return; } - addressListViewModel.setAddress(item); + widget.addressListViewModel.setAddress(item); }, onEdit: editable - ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item) + ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) { + updateItems(); // refresh the new address + }) : null, + isHidden: item.isHidden, + onHide: () => _hideAddress(item), ); }); } + } - return index != 0 - ? cell - : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), topRight: Radius.circular(30)), - child: cell, - ); - }, - ), + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), topRight: Radius.circular(30)), + child: cell, + ); + }, ); } + + void _hideAddress(WalletAddressListItem item) async { + await widget.addressListViewModel.toggleHideAddress(item); + updateItems(); + } + } diff --git a/lib/src/screens/receive/widgets/header_tile.dart b/lib/src/screens/receive/widgets/header_tile.dart index faaa9ed07..dc48db89d 100644 --- a/lib/src/screens/receive/widgets/header_tile.dart +++ b/lib/src/screens/receive/widgets/header_tile.dart @@ -10,6 +10,7 @@ class HeaderTile extends StatefulWidget { this.showSearchButton = false, this.showTrailingButton = false, this.trailingButtonTap, + this.onSearchCallback, this.trailingIcon, }); @@ -18,6 +19,7 @@ class HeaderTile extends StatefulWidget { final bool showSearchButton; final bool showTrailingButton; final VoidCallback? trailingButtonTap; + final VoidCallback? onSearchCallback; final Icon? trailingIcon; @override @@ -41,7 +43,10 @@ class _HeaderTileState extends State { _isSearchActive ? Expanded( child: TextField( - onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value), + onChanged: (value) { + widget.walletAddressListViewModel.updateSearchText(value); + widget.onSearchCallback?.call(); + }, cursorColor: Theme.of(context).extension()!.tilesTextColor, cursorWidth: 0.5, decoration: InputDecoration( diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 9f0db059a..9d09e57a1 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -37,6 +37,10 @@ class QRWidget extends StatelessWidget { final int? qrVersion; final String? heroTag; + PaymentURI get addressUri { + return addressListViewModel.uri; + } + @override Widget build(BuildContext context) { final copyImage = Image.asset('assets/images/copy_address.png', @@ -77,14 +81,14 @@ class QRWidget extends StatelessWidget { () async { await Navigator.pushNamed(context, Routes.fullscreenQR, arguments: QrViewData( - data: addressListViewModel.uri.toString(), + data: addressUri.toString(), heroTag: heroTag, )); }, ); }, child: Hero( - tag: Key(heroTag ?? addressListViewModel.uri.toString()), + tag: Key(heroTag ?? addressUri.toString()), child: Center( child: AspectRatio( aspectRatio: 1.0, @@ -105,7 +109,7 @@ class QRWidget extends StatelessWidget { color: Colors.white, ), ), - child: QrImage(data: addressListViewModel.uri.toString())), + child: QrImage(data: addressUri.toString())), ), ), ), @@ -148,7 +152,7 @@ class QRWidget extends StatelessWidget { builder: (context) => Observer( builder: (context) => GestureDetector( onTap: () { - Clipboard.setData(ClipboardData(text: addressListViewModel.address.address)); + Clipboard.setData(ClipboardData(text: addressUri.address)); showBar(context, S.of(context).copied_to_clipboard); }, child: Row( @@ -157,7 +161,7 @@ class QRWidget extends StatelessWidget { children: [ Expanded( child: Text( - addressListViewModel.address.address, + addressUri.address, textAlign: TextAlign.center, style: TextStyle( fontSize: 15, diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index d63f78224..df6cbdb9f 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -27,32 +27,41 @@ abstract class ContactListViewModelBase with Store { settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) { - info.addressInfos!.forEach((key, value) { - final nextUnusedAddress = value.firstWhereOrNull( - (addressInfo) => !(info.usedAddresses?.contains(addressInfo.address) ?? false)); - if (nextUnusedAddress != null) { - final name = _createName(info.name, nextUnusedAddress.label); - walletContacts.add(WalletContact( - nextUnusedAddress.address, - name, - walletTypeToCryptoCurrency(info.type), - )); - } - }); + final key = info.addressInfos!.keys.first; + final value = info.addressInfos![key]; + final address = value?.first; + if (address != null) { + final name = _createName(info.name, address.label); + walletContacts.add(WalletContact( + address.address, + name, + walletTypeToCryptoCurrency(info.type), + )); + } } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { - info.addresses!.forEach((address, label) { - if (label.isEmpty) { - return; - } - final name = _createName(info.name, label); + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) { + final address = info.address; + final name = _createName(info.name, ""); walletContacts.add(WalletContact( address, name, - walletTypeToCryptoCurrency(info.type, - isTestnet: - info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + walletTypeToCryptoCurrency(info.type), )); - }); + } else { + info.addresses!.forEach((address, label) { + if (label.isEmpty) { + return; + } + final name = _createName(info.name, label); + walletContacts.add(WalletContact( + address, + name, + walletTypeToCryptoCurrency(info.type, + isTestnet: + info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + )); + }); + } } else { walletContacts.add(WalletContact( info.address, diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index ca56750f0..d29b7df6b 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -121,7 +121,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositAmount = ''; receiveAmount = ''; receiveAddress = ''; - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; provider = providersForCurrentPair().first; final initialProvider = provider; provider!.checkIsAvailable().then((bool isAvailable) { @@ -155,6 +155,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash; + bool get hideAddressAfterExchange => + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + bool _useTorOnly; final Box trades; final ExchangeTemplateStore _exchangeTemplateStore; @@ -540,6 +544,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with isFixedRate: isFixedRateMode, ); + if (hideAddressAfterExchange) { + wallet.walletAddresses.hiddenAddresses.add(depositAddress); + await wallet.walletAddresses.saveAddressesInBox(); + } + var amount = isFixedRateMode ? receiveAmount : depositAmount; amount = amount.replaceAll(',', '.'); @@ -603,8 +612,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with isReceiveAmountEntered = false; depositAmount = ''; receiveAmount = ''; - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : ''; - receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; + receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; isDepositAddressEnabled = !(depositCurrency == wallet.currency); isFixedRateMode = false; _onPairChange(); 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 2edda3d29..d365c8e00 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 @@ -78,6 +78,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { wallet, accountIndex: monero!.getCurrentAccount(wallet).id, label: label); + final addr = await monero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } @@ -88,6 +90,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { wallet, accountIndex: wownero!.getCurrentAccount(wallet).id, label: label); + final addr = await wownero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } diff --git a/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart b/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart new file mode 100644 index 000000000..74c7605e9 --- /dev/null +++ b/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart @@ -0,0 +1,3 @@ +import 'package:cake_wallet/utils/list_item.dart'; + +class WalletAddressHiddenListHeader extends ListItem {} \ No newline at end of file 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 6a6e34113..725b1ddbf 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,7 +1,7 @@ import 'package:cake_wallet/utils/list_item.dart'; class WalletAddressListItem extends ListItem { - const WalletAddressListItem({ + WalletAddressListItem({ required this.address, required this.isPrimary, this.id, @@ -11,6 +11,8 @@ class WalletAddressListItem extends ListItem { this.isChange = false, // Address that is only ever used once, shouldn't be used to receive funds, copy and paste, share etc this.isOneTimeReceiveAddress = false, + this.isHidden = false, + this.isManual = false, }) : super(); final int? id; @@ -20,6 +22,8 @@ class WalletAddressListItem extends ListItem { final int? txCount; final String? balance; final bool isChange; + bool isHidden; + bool isManual; final bool? isOneTimeReceiveAddress; @override 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 17fc4b849..9fb7509eb 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 @@ -18,12 +18,16 @@ import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/tron/tron.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_hidden_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:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/api/wallet.dart'; +import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; @@ -271,56 +275,40 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType get type => wallet.type; @computed - WalletAddressListItem get address => - WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); + WalletAddressListItem get address { + return WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); + } @computed PaymentURI get uri { - if (wallet.type == WalletType.monero) { - return MoneroURI(amount: amount, address: address.address); + switch (wallet.type) { + case WalletType.monero: + return MoneroURI(amount: amount, address: address.address); + case WalletType.haven: + return HavenURI(amount: amount, address: address.address); + case WalletType.bitcoin: + return BitcoinURI(amount: amount, address: address.address); + case WalletType.litecoin: + return LitecoinURI(amount: amount, address: address.address); + case WalletType.ethereum: + return EthereumURI(amount: amount, address: address.address); + case WalletType.bitcoinCash: + return BitcoinCashURI(amount: amount, address: address.address); + case WalletType.banano: + return NanoURI(amount: amount, address: address.address); + case WalletType.nano: + return NanoURI(amount: amount, address: address.address); + case WalletType.polygon: + return PolygonURI(amount: amount, address: address.address); + case WalletType.solana: + return SolanaURI(amount: amount, address: address.address); + case WalletType.tron: + return TronURI(amount: amount, address: address.address); + case WalletType.wownero: + return WowneroURI(amount: amount, address: address.address); + case WalletType.none: + throw Exception('Unexpected type: ${type.toString()}'); } - - if (wallet.type == WalletType.haven) { - return HavenURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.bitcoin) { - return BitcoinURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.litecoin) { - return LitecoinURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.ethereum) { - return EthereumURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.bitcoinCash) { - return BitcoinCashURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.nano) { - return NanoURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.polygon) { - return PolygonURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.solana) { - return SolanaURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.tron) { - return TronURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.wownero) { - return WowneroURI(amount: amount, address: address.address); - } - - throw Exception('Unexpected type: ${type.toString()}'); } @computed @@ -341,7 +329,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo id: subaddress.id, isPrimary: isPrimary, name: subaddress.label, - address: subaddress.address); + address: subaddress.address, + balance: subaddress.received, + txCount: subaddress.txCount, + ); }); addressList.addAll(addressItems); } @@ -468,6 +459,16 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + for (var i = 0; i < addressList.length; i++) { + if (!(addressList[i] is WalletAddressListItem)) continue; + (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses.contains((addressList[i] as WalletAddressListItem).address); + } + + for (var i = 0; i < addressList.length; i++) { + if (!(addressList[i] is WalletAddressListItem)) continue; + (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses.contains((addressList[i] as WalletAddressListItem).address); + } + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { @@ -479,7 +480,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return addressList; } - + Future toggleHideAddress(WalletAddressListItem item) async { + if (item.isHidden) { + wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address); + } else { + wallet.walletAddresses.hiddenAddresses.add(item.address); + } + await wallet.walletAddresses.saveAddressesInBox(); + if (wallet.type == WalletType.monero) { + monero!.getSubaddressList(wallet).update(wallet, accountIndex: monero!.getCurrentAccount(wallet).id); + } else if (wallet.type == WalletType.wownero) { + wownero!.getSubaddressList(wallet).update(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id); + } else if (wallet.type == WalletType.haven) { + haven!.getSubaddressList(wallet).update(wallet, accountIndex: haven!.getCurrentAccount(wallet).id); + } + } @observable bool hasAccounts; @@ -515,6 +530,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash; + @computed + bool get isBalanceAvailable => isElectrumWallet; + + @computed + bool get isReceivedAvailable => + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + @computed bool get isSilentPayments => wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet); @@ -524,6 +547,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled && !isSilentPayments; + @computed + bool get showAddManualAddresses => + !isAutoGenerateSubaddressEnabled || + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + List _baseItems; final YatStore yatStore; @@ -542,6 +571,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void _init() { _baseItems = []; + if (wallet.walletAddresses.hiddenAddresses.isNotEmpty) { + _baseItems.add(WalletAddressHiddenListHeader()); + } + if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) { @@ -551,6 +584,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type != WalletType.nano && wallet.type != WalletType.banano) { _baseItems.add(WalletAddressListHeader()); } + if (wallet.isEnabledAutoGenerateSubaddress) { + wallet.walletAddresses.address = wallet.walletAddresses.latestAddress; + } } @action diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 0cb4b2f11..324db7548 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -333,6 +333,8 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "help", + "hide": "Hide", + "hidden_addresses": "Hidden Addresses", "hidden_balance": "Hidden Balance", "hide_details": "Hide Details", "high_contrast_theme": "High Contrast Theme", @@ -683,6 +685,7 @@ "setup_your_debit_card": "Set up your debit card", "share": "Share", "share_address": "Share address", + "show": "Show", "shared_seed_wallet_groups": "Shared Seed Wallet Groups", "show_details": "Show Details", "show_keys": "Show seed/keys", diff --git a/tool/configure.dart b/tool/configure.dart index 362a6d5d6..d54ec153d 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -303,10 +303,14 @@ class Subaddress { Subaddress({ required this.id, required this.label, - required this.address}); + required this.address, + required this.received, + required this.txCount}); final int id; final String label; final String address; + final String? received; + final int txCount; } class MoneroBalance extends Balance {